01What does Liskov substitution ask of a child class?
It must keep every promise of its parent, so code written for the parent also works with the child.
02Open-closed: how do you add a new behaviour?
Add a new class that plugs into an existing interface. Do not edit code that already works.
03In a class diagram, what does ╌╌▷ mean?
The class at the tail implements the interface at the head.
A restaurant on a food delivery app prints a kitchen ticket for every new order. The ticket printer is a small box that can only print.
The code, though, describes it with the same interface as the big office machine in the back room: print, scan and fax. So the little printer must pretend to scan and fax. This lesson is about why that pretending hurts, and the small fix.
The problem
Here is the design today. One interface, Machine, lists everything a machine in the restaurant can do. The ticket printer implements it.
interface Machine {
void printDoc(String doc);
void scanDoc(String doc);
void faxDoc(String doc, String number);
}
class TicketPrinter implements Machine {
public void printDoc(String doc) {
System.out.println("Printing " + doc);
}
public void scanDoc(String doc) {
throw new UnsupportedOperationException("no scanner");
}
public void faxDoc(String doc, String number) {
throw new UnsupportedOperationException("no fax");
}
}It works. printDoc("ticket A17") prints Printing ticket A17. Two of the three methods are fakes that only throw.
┌────────────────────────────────────────────┐ │ │ │ ┌───────────────────────┐ │ │ │ <<interface>> Machine │ │ │ ├───────────────────────┤ │ │ │ printDoc(doc) │ │ │ │ scanDoc(doc) │ │ │ │ faxDoc(doc, number) │ │ │ └───────────────────────┘ │ │ △ △ │ │ ╎ ╎ │ │ ┌───────┴───────┐ ┌───────┴───────┐ │ │ │ OfficeMachine │ │ TicketPrinter │ │ │ ├───────────────┤ ├───────────────┤ │ │ │ printDoc │ │ printDoc │ │ │ │ scanDoc │ │ scanDoc × │ │ │ │ faxDoc │ │ faxDoc × │ │ │ └───────────────┘ └───────────────┘ │ │ ╌╌▷ implements × throws an error │ │ │ └────────────────────────────────────────────┘
The change request
The accounts team asks for one new feature on the office machine: scan an invoice straight to email. Someone adds scanToEmail(doc, email) to Machine. Then this happens:
TicketPrinterstops compiling in Java and TypeScript. In Python, creating one fails with aTypeError.- Someone must open
TicketPrinterand add a third fake method that throws. - Every class that only prints gets the same edit, and every test around it runs again.
The ticket printer never scans. A scanner feature still forced a change in it.
There is a second hurt, at run time. Code that receives a Machine can call faxDoc on the ticket printer. The compiler allows it, and the app crashes with no fax.
That is the Liskov break from the last lesson. The interface promises a fax, and this class cannot keep the promise.
The idea
No class should depend on methods it does not use. Split a wide interface into small role interfaces, one per job a client needs.
A is named after what a client asks for, not after the device. The kitchen asks for printing. The accounts team asks for scanning.
┌────────────────────────────────────────────────────┐ │ │ │ ┌────────────────┐ │ │ │ KitchenService │ │ │ └──────┬─────────┘ │ │ │ uses │ │ ▼ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ Printer │ │ Scanner │ │ Fax │ │ │ ├───────────┤ ├───────────┤ ├───────────┤ │ │ │ printDoc │ │ scanDoc │ │ faxDoc │ │ │ └───────────┘ └───────────┘ └───────────┘ │ │ △ △ △ △ │ │ ╎ └╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ │ │ ╎ ╎ │ │ ┌──┴────────────┐ ┌───┴───────────┐ │ │ │ TicketPrinter │ │ OfficeMachine │ │ │ ├───────────────┤ ├───────────────┤ │ │ │ printDoc │ │ printDoc │ │ │ └───────────────┘ │ scanDoc │ │ │ │ faxDoc │ │ │ └───────────────┘ │ │ top row: interfaces ╌╌▷ implements ──▶ uses │ │ │ └────────────────────────────────────────────────────┘
- Printer, Scanner, Fax: one method each. Each one is a promise that every class behind it can keep.
- TicketPrinter implements
Printeronly. No fake methods. - OfficeMachine implements all three. A class can play many roles.
- KitchenService holds a
Printer. It cannot callfaxDoc, because aPrinterhas no such method.
The fix
Step 1: split the interface
interface Printer { void printDoc(String doc); }
interface Scanner { void scanDoc(String doc); }
interface Fax { void faxDoc(String doc, String number); }
class TicketPrinter implements Printer {
public void printDoc(String doc) {
System.out.println("Printing " + doc);
}
}
class OfficeMachine implements Printer, Scanner, Fax {
public void printDoc(String doc) {
System.out.println("Printing " + doc);
}
public void scanDoc(String doc) {
System.out.println("Scanning " + doc);
}
public void faxDoc(String doc, String number) {
System.out.println("Faxing " + doc + " to " + number);
}
}In Python, a class can inherit from several abstract base classes, so OfficeMachine(Printer, Scanner, Fax) plays the same part as implements in Java and TypeScript.
Step 2: let the client ask for one role
class KitchenService {
private final Printer printer;
KitchenService(Printer printer) {
this.printer = printer;
}
void newOrder(String orderId) {
printer.printDoc("ticket " + orderId);
}
}The kitchen now accepts any Printer. Give it a TicketPrinter or an OfficeMachine, and order A17 prints Printing ticket A17 with either one.
The change request again
Add scanToEmail(doc, email) to Scanner, and write it in OfficeMachine. That is the whole change.
TicketPrinterdoes not change. It never implementedScanner.KitchenServicedoes not change. It only knowsPrinter.- The change stays with the classes that scan.
How it runs
- Order app to KitchenService:newOrder(A17)
- KitchenService to TicketPrinter:printDoc(...)
- TicketPrinter to KitchenService:done
- Order app to KitchenService:newOrder(A18)
- KitchenService to OfficeMachine:printDoc(...)
- OfficeMachine to KitchenService:done
The kitchen talks to both machines through one method. The office machine can do much more, but the kitchen does not see that, and does not need to.
When to use it
Signals in the code
- A method that throws "not supported", returns nothing useful, or has an empty body because the interface forced it.
- An interface where each client calls a different small part of it.
- A change for one client makes you edit, rebuild or retest classes that another client uses.
- A name like
Machine,ManagerorServicewith ten or more methods.
How to split
- List the clients of the interface.
- Write down which methods each client calls.
- Make one interface per group, named after the job:
Printer,Scanner. - When one client needs several roles, join them: in Java,
interface AllInOne extends Printer, Scanner, Fax {}.
When it is too much
Do not make one interface per method by habit. If every client of an interface calls all of its methods, the interface is already the right size.
A wide interface can also be a deliberate trade. The Java Collections designers kept add and remove on every Collection, and let read-only collections throw UnsupportedOperationException.
Their design FAQ explains why. Separate interfaces for modifiable, fixed-size, append-only and immutable collections would have grown the core from four interfaces to about twenty.
Interface segregation or single responsibility?
| Single responsibility | Interface segregation | |
|---|---|---|
| Looks at | one class | one interface |
| Asks | how many reasons to change? | what does each client use? |
| Smell | one class, many jobs | fake or throwing methods |
| Fix | split the class | split the interface |
The two often travel together. A class with one job usually needs only a small interface.
- 01
No class should depend on methods it does not use. Split a wide interface into small role interfaces.
- 02
A method that throws "not supported" is the clearest sign of an interface that is too wide.
- 03
Name each interface after what a client asks for (Printer, Scanner), not after the device. One class can implement many roles.
- 04
After the split, a change to one role only touches the classes that play that role.
Read more
- Interface segregation principle on Wikipedia, with the Xerox printer system where Robert C. Martin first used it
- Role interface by Martin Fowler, on naming interfaces after what clients need
- collections.abc in the Python docs, small interfaces joined into bigger ones
- Java Collections design FAQ, the case for a wide interface that sometimes throws
Next: dependency inversion, the last SOLID principle. You pass a payment gateway into a class instead of creating it inside.