Return

SOLID: Nguyên Tắc Phân Tách Interface (Interface Segregation Principle)

Written by LocLT

Tiếp tục hành trình khám phá SOLID, hôm nay chúng ta sẽ tìm hiểu chữ cái thứ tư: I - Interface Segregation Principle (ISP). Đây là nguyên tắc giúp bạn thiết kế interface “gọn gàng” và tránh việc các class phải implement những method mà chúng không cần.

Robert C. Martin phát biểu nguyên tắc này như sau:

“Clients should not be forced to depend on interfaces they do not use.”

Nói cách khác: Đừng ép class implement những method mà nó không cần. Thay vì có một interface “béo” chứa tất cả method, hãy tách thành nhiều interface nhỏ, chuyên biệt.

Tại Sao Điều Này Quan Trọng?

Khi interface quá “béo”, các class implement nó sẽ phải viết code cho những method không liên quan. Điều này dẫn đến code thừa, khó bảo trì, và khi interface thay đổi, tất cả các class implement đều bị ảnh hưởng - kể cả những class không dùng method đó.

Ví Dụ Thực Tế: Hệ Thống Máy In Văn Phòng

Hãy xem một ví dụ thực tế về hệ thống quản lý máy in trong văn phòng.

Ví dụ Xấu: Vi phạm ISP

Một công ty thiết kế interface cho tất cả thiết bị in ấn. Vấn đề là: máy in cơ bản không thể scan hay fax!


// VI PHẠM: Interface "béo" - chứa quá nhiều method
interface MultiFunctionDevice {
    void print(Document doc);
    void scan(Document doc);
    void fax(Document doc);
    void photocopy(Document doc);
    void staple(Document doc);
}

// Máy in văn phòng cao cấp - OK, implement được hết
class OfficeMultiFunctionPrinter implements MultiFunctionDevice {
    @Override
    public void print(Document doc) {
        System.out.println("In tài liệu: " + doc.getName());
    }

    @Override
    public void scan(Document doc) {
        System.out.println("Scan tài liệu: " + doc.getName());
    }

    @Override
    public void fax(Document doc) {
        System.out.println("Fax tài liệu: " + doc.getName());
    }

    @Override
    public void photocopy(Document doc) {
        System.out.println("Photocopy tài liệu: " + doc.getName());
    }

    @Override
    public void staple(Document doc) {
        System.out.println("Đóng ghim tài liệu: " + doc.getName());
    }
}

// Máy in cơ bản - BỊ ÉP implement những thứ không thể làm!
class BasicPrinter implements MultiFunctionDevice {
    @Override
    public void print(Document doc) {
        System.out.println("In tài liệu: " + doc.getName());
    }

    // VI PHẠM: Bị ép implement những method không hỗ trợ
    @Override
    public void scan(Document doc) {
        throw new UnsupportedOperationException("Máy in cơ bản không hỗ trợ scan!");
    }

    @Override
    public void fax(Document doc) {
        throw new UnsupportedOperationException("Máy in cơ bản không hỗ trợ fax!");
    }

    @Override
    public void photocopy(Document doc) {
        throw new UnsupportedOperationException("Máy in cơ bản không hỗ trợ photocopy!");
    }

    @Override
    public void staple(Document doc) {
        throw new UnsupportedOperationException("Máy in cơ bản không có bộ đóng ghim!");
    }
}

Thiết kế này có nhiều vấn đề:

  • BasicPrinter bị ép implement 4 method mà nó không thể thực hiện.
  • Người dùng MultiFunctionDevice không biết method nào sẽ ném exception.
  • Khi thêm method mới vào interface (ví dụ: printDoubleSided()), TẤT CẢ các class đều phải sửa.

Ví dụ Tốt: Tuân Thủ ISP

Giải pháp là tách interface “béo” thành nhiều interface nhỏ, mỗi interface phục vụ một mục đích cụ thể.


// Interface nhỏ gọn cho từng chức năng
interface Printable {
    void print(Document doc);
}

interface Scannable {
    void scan(Document doc);
}

interface Faxable {
    void fax(Document doc);
}

interface Photocopiable {
    void photocopy(Document doc);
}

interface Stapleable {
    void staple(Document doc);
}

// Máy in cơ bản - chỉ implement những gì nó làm được
class BasicPrinter implements Printable {
    @Override
    public void print(Document doc) {
        System.out.println("In tài liệu: " + doc.getName());
    }
    // Không có code thừa, không có exception!
}

// Máy in có scan - implement 2 interface
class PrinterWithScanner implements Printable, Scannable {
    @Override
    public void print(Document doc) {
        System.out.println("In tài liệu: " + doc.getName());
    }

    @Override
    public void scan(Document doc) {
        System.out.println("Scan tài liệu: " + doc.getName());
    }
}

// Máy đa chức năng cao cấp - implement tất cả những gì nó hỗ trợ
class AdvancedOfficeMachine implements Printable, Scannable, Faxable, Photocopiable, Stapleable {
    @Override
    public void print(Document doc) {
        System.out.println("In tài liệu: " + doc.getName());
    }

    @Override
    public void scan(Document doc) {
        System.out.println("Scan tài liệu: " + doc.getName());
    }

    @Override
    public void fax(Document doc) {
        System.out.println("Fax tài liệu: " + doc.getName());
    }

    @Override
    public void photocopy(Document doc) {
        System.out.println("Photocopy tài liệu: " + doc.getName());
    }

    @Override
    public void staple(Document doc) {
        System.out.println("Đóng ghim tài liệu: " + doc.getName());
    }
}

Code sử dụng giờ đây rõ ràng và an toàn:


// Service chỉ cần biết về khả năng cần thiết
class DocumentService {
    
    // Chỉ cần thiết bị có thể in
    public void printDocument(Printable printer, Document doc) {
        printer.print(doc);
    }

    // Chỉ cần thiết bị có thể scan
    public void scanDocument(Scannable scanner, Document doc) {
        scanner.scan(doc);
    }

    // Cần cả in lẫn scan
    public  void copyDocument(T device, Document doc) {
        device.scan(doc);
        device.print(doc);
    }
}

// Sử dụng
DocumentService service = new DocumentService();
BasicPrinter basicPrinter = new BasicPrinter();
PrinterWithScanner combo = new PrinterWithScanner();

service.printDocument(basicPrinter, doc); // OK
// service.scanDocument(basicPrinter, doc); // Compile error! BasicPrinter không implement Scannable

service.printDocument(combo, doc); // OK
service.scanDocument(combo, doc);  // OK
service.copyDocument(combo, doc);  // OK

Lợi Ích Của Thiết Kế Này

  • Không code thừa: Mỗi class chỉ implement đúng những gì nó cần.
  • Type-safe: Compiler sẽ báo lỗi nếu bạn cố dùng chức năng mà thiết bị không hỗ trợ.
  • Dễ mở rộng: Thêm interface mới không ảnh hưởng đến các class hiện có.
  • Dễ test: Mock interface nhỏ dễ hơn mock interface “béo”.

Khi Nào Nên Tách Interface?

Hãy xem xét tách interface khi:

  1. Nhiều class implement interface nhưng để trống hoặc throw exception cho một số method.
  2. Interface có quá nhiều method không liên quan đến nhau.
  3. Khi thay đổi interface, nhiều class không dùng method đó cũng bị ảnh hưởng.
  4. Bạn thấy mình hay dùng instanceof để kiểm tra “class này có hỗ trợ method kia không?”.

Kết Luận

Nguyên tắc Interface Segregation giúp bạn thiết kế interface “vừa vặn” với nhu cầu thực tế. Thay vì một interface khổng lồ làm tất cả, hãy tạo nhiều interface nhỏ, mỗi interface phục vụ một mục đích rõ ràng. Điều này tuân theo triết lý “làm ít nhưng làm tốt” - một interface nên chỉ định nghĩa những gì thực sự cần thiết cho client sử dụng nó.