Concepts / SOLID and design principles / Interface segregation

Interface segregation

Small interfaces so classes are not forced to fake methods.

01The intuition

The problem it solves

A school app has one big Device interface because every gadget in the lab is "a device": printers, scanners, a projector and a smart board.

from abc import ABC, abstractmethod

class Device(ABC):
    @abstractmethod
    def print_page(self, page): ...
    @abstractmethod
    def scan_page(self): ...
    @abstractmethod
    def project(self, image): ...

class Projector(Device):
    def project(self, image): print("showing", image)
    def print_page(self, page): raise NotImplementedError
    def scan_page(self): raise NotImplementedError

The projector is forced to fake two methods it cannot do. Code that receives a Device cannot tell which methods are safe to call. When someone adds fax_page to the interface, every one of the eight device classes must change, including the projector, which will never fax anything. The interface is fat: it bundles abilities that do not travel together.

The interface segregation principle says: no class should be forced to depend on methods it does not use. In practice, prefer several small interfaces, one per ability, over one large one.

Build it one piece at a time

Step 1: split the fat interface along the abilities. Each ability becomes its own one-or-two-method interface.

class Printer(ABC):
    @abstractmethod
    def print_page(self, page) -> None: ...

class Scanner(ABC):
    @abstractmethod
    def scan_page(self): ...

class Projector(ABC):
    @abstractmethod
    def project(self, image) -> None: ...

Step 2: let each class claim only the abilities it has. A multifunction printer implements two; the projector implements one; nothing fakes anything.

class LabProjector(Projector):
    def project(self, image) -> None:
        print("showing", image)

class OfficeMfp(Printer, Scanner):
    def print_page(self, page) -> None: print("printing", page)
    def scan_page(self): return "scanned bytes"

Step 3: make callers ask for the smallest ability they need. A function that only prints takes a Printer. It can now accept the MFP, a cheap printer, or a fake in a test, and it cannot accidentally call scan_page.

def print_homework(printer: Printer, pages: list) -> None:
    for p in pages:
        printer.print_page(p)

def archive(scanner: Scanner, store) -> None:
    store.save(scanner.scan_page())

Step 4: add an ability without touching the others. Fax becomes a new interface; only devices that can fax implement it, and no projector is edited.

class Fax(ABC):
    @abstractmethod
    def fax_page(self, page, number: str) -> None: ...

class OfficeMfp(Printer, Scanner, Fax):
    def print_page(self, page) -> None: print("printing", page)
    def scan_page(self): return "scanned bytes"
    def fax_page(self, page, number: str) -> None: print("faxing to", number)
StepMethods that raise NotImplementedClasses edited to add faxCan print_homework take a fake
Start2 in the projector alone, more elsewhereall 8only a fake with 3 methods
1 and 20not yet addeda fake with 1 method
3 and 401yes

Why it works

A small interface is a precise promise. A class that implements it can keep every method honestly, which is exactly the condition Liskov substitution needs. A caller that depends on a small interface is coupled to one ability, so changes to other abilities cannot reach it. The invariant is "every method on an interface is meaningful for every implementer", and you can check it by reading the implementations for raise-or-do-nothing bodies.

Where it breaks: slicing so thin that every method is its own interface and a simple object implements nine of them. Group methods that always travel together, such as open and close, and separate the ones that do not.

The trade-off you must say out loud

More interfaces means more names to keep in your head and sometimes a class header that lists five of them. Say the test you used: "I split where some implementers would have to fake a method". The follow-up is often "isn't a role interface per client overkill", and the answer is that you split by ability, not by caller, which keeps the count small and the meaning obvious.

Where it shows up in the 100 Google problems

02Used in these problems