Interface segregation

Small, focused interfaces, so no class must pretend to do work it cannot do.

Pattern: SOLID20 min read
Quick recall3 questions
  1. 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.

  2. 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.

  3. 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.

One wide interface. The ticket printer must implement all three methods, and two of them only throw.

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:

  1. TicketPrinter stops compiling in Java and TypeScript. In Python, creating one fails with a TypeError.
  2. Someone must open TicketPrinter and add a third fake method that throws.
  3. 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.

Three small interfaces on top. Each class implements only the roles it can really do, and the kitchen uses only Printer.
  • Printer, Scanner, Fax: one method each. Each one is a promise that every class behind it can keep.
  • TicketPrinter implements Printer only. No fake methods.
  • OfficeMachine implements all three. A class can play many roles.
  • KitchenService holds a Printer. It cannot call faxDoc, because a Printer has 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.

  • TicketPrinter does not change. It never implemented Scanner.
  • KitchenService does not change. It only knows Printer.
  • The change stays with the classes that scan.

How it runs

Order appa new order arrives
KitchenServiceneeds a Printer
TicketPrinteronly prints
OfficeMachineprints, scans, faxes
  1. Order app to KitchenService:
    newOrder(A17)
  2. KitchenService to TicketPrinter:
    printDoc(...)
  3. TicketPrinter to KitchenService:
    done
  4. Order app to KitchenService:
    newOrder(A18)
  5. KitchenService to OfficeMachine:
    printDoc(...)
  6. OfficeMachine to KitchenService:
    done
Read it top to bottom. For order A18 the kitchen is set up with the office machine. It makes the same call, and never sees scan or fax.

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, Manager or Service with ten or more methods.

How to split

  1. List the clients of the interface.
  2. Write down which methods each client calls.
  3. Make one interface per group, named after the job: Printer, Scanner.
  4. 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 responsibilityInterface segregation
Looks atone classone interface
Askshow many reasons to change?what does each client use?
Smellone class, many jobsfake or throwing methods
Fixsplit the classsplit the interface

The two often travel together. A class with one job usually needs only a small interface.

Remember4 points
  1. 01

    No class should depend on methods it does not use. Split a wide interface into small role interfaces.

  2. 02

    A method that throws "not supported" is the clearest sign of an interface that is too wide.

  3. 03

    Name each interface after what a client asks for (Printer, Scanner), not after the device. One class can implement many roles.

  4. 04

    After the split, a change to one role only touches the classes that play that role.

Read more

Next: dependency inversion, the last SOLID principle. You pass a payment gateway into a class instead of creating it inside.