Low-level designCore9 min

Interface segregation principle

No code should be forced to depend on methods it doesn't use. Shape interfaces around the clients that call them, so each class implements only what it can do and each caller sees only what it needs.

Watch it evolve1 / 10
The problem

Two kinds of message, one interface

Channel has send() and sendFile(). Reminders sends texts, and LabResults sends files, through each patient's channel.

80
Start
Stage 1
Final

1 · The situation

One channel interface for every message

A small clinic sends patients two kinds of message: a reminder before each appointment, and a note when lab results are ready, with the results attached. Each patient picks how the clinic reaches them: Ana by text message, Ben by email.

The first version has one interface, Channel, for everything a channel might do: send() a text, and sendFile() with an attachment. SmsChannel and EmailChannel implement it. Two clients use it: Reminders sends a reminder through the patient's channel, and LabResults sends the results through the same channel. Python writes send_file() and send_results(), and in every language printing stands in for the real network call.

The first versionJava
interface Channel {
    void send(String to, String text);
    void sendFile(String to, String text, String file);
}

class SmsChannel implements Channel {
    public void send(String to, String text) {
        System.out.println("SMS to " + to + ": " + text);
    }

    public void sendFile(String to, String text, String file) {
    }
}

class EmailChannel implements Channel {
    public void send(String to, String text) {
        System.out.println("Email to " + to + ": " + text);
    }

    public void sendFile(String to, String text, String file) {
        System.out.println("Email to " + to + ": " + text + " [" + file + "]");
    }
}

record Patient(String name, Channel channel, String address, String email) {}

class Reminders {
    void remind(Patient patient, String when) {
        Channel channel = patient.channel();
        channel.send(patient.address(), "Reminder: checkup " + when);
    }
}

class LabResults {
    private int sent;

    void sendResults(Patient patient, String file) {
        Channel channel = patient.channel();
        channel.sendFile(patient.address(), "Your lab results are ready", file);
        sent++;
    }

    int sent() {
        return sent;
    }
}

public class Main {
    public static void main(String[] args) {
        Channel sms = new SmsChannel();
        Channel email = new EmailChannel();
        Patient ana = new Patient("Ana", sms, "555-0142", "ana@example.com");
        Patient ben = new Patient("Ben", email, "ben@example.com", "ben@example.com");

        Reminders reminders = new Reminders();
        reminders.remind(ana, "on Tuesday at 9:30 am");
        reminders.remind(ben, "on Wednesday at 2:00 pm");

        LabResults results = new LabResults();
        results.sendResults(ana, "results-ana.pdf");
        results.sendResults(ben, "results-ben.pdf");
        System.out.println("Lab results sent: " + results.sent());
    }
}
«interface»Channel+ send(String, String)+ sendFile(String, String, String)SmsChannel+ send(String, String)+ sendFile(String, String, String)EmailChannel+ send(String, String)+ sendFile(String, String, String)«record»Patient− name: String− channel: Channel− address: String− email: StringReminders~ remind(Patient, String)LabResults− sent: int~ sendResults(Patient, String)~ sent(): int

The class diagram of this version, drawn from its code: both clients depend on the whole Channel interface.

It runs, and the count at the end looks right:

SMS to 555-0142: Reminder: checkup on Tuesday at 9:30 am
Email to ben@example.com: Reminder: checkup on Wednesday at 2:00 pm
Email to ben@example.com: Your lab results are ready [results-ben.pdf]
Lab results sent: 2

Two results sent, by the count. Only Ben's arrived.

2 · The change

A text message can't carry a file

Ana never got her results. A text message can't carry a PDF, but SmsChannel had to implement sendFile() to be a Channel, so its author left the method empty:

The method SMS can't supportJava
class SmsChannel implements Channel {
    public void send(String to, String text) {
        System.out.println("SMS to " + to + ": " + text);
    }

    public void sendFile(String to, String text, String file) {
    }
}

LabResults called it, counted the results as sent, and nothing went out. Making the method throw instead would be louder, but no better: LabResults would still be handed a channel that can't do what it asks, and the clinic would learn it from a failure in the middle of a batch.

The wide interface costs the code around it too, including the parts it doesn't break:

Who

What it needs

What Channel makes it depend on

Reminders, a client

send()

sendFile() as well: a test fake for it must implement both, and a change to sendFile() reaches it

LabResults, a client

sendFile()

Any Channel, including one that can't send a file

SmsChannel, an implementer

To send text

A sendFile() it can only leave empty or make throw

The first row is easy to check. In Java, a fake channel for testing Reminders that has only send() doesn't compile: "FakeChannel is not abstract and does not override abstract method sendFile(String,String,String) in Channel".

The practice checks flag the interface: SmsChannel leaves one of Channel's methods empty.

3 · The fix

Split the interface by what each client needs

Start from the first version, and look at Channel from its clients' side. Reminders needs to send a text, and LabResults needs to send a file. Give each need its own interface:

TextSender has send(), and FileSender has sendFile(). In JavaScript they're two documented shapes, and in Python two Protocol classes.

Java
interface TextSender {
    void send(String to, String text);
}

interface FileSender {
    void sendFile(String to, String text, String file);
}
«interface»TextSender+ send(String, String)«interface»FileSender+ sendFile(String, String, String)SmsChannel+ send(String, String)EmailChannel+ send(String, String)+ sendFile(String, String, String)«record»Patient− name: String− channel: TextSender− address: String− email: StringReminders~ remind(Patient, String)LabResults− sender: FileSender− sent: int~ sendResults(Patient, String)~ sent(): int

The types now say what each job needs. Ana's results go to her email address, and both notices arrive:

SMS to 555-0142: Reminder: checkup on Tuesday at 9:30 am
Email to ben@example.com: Reminder: checkup on Wednesday at 2:00 pm
Email to ana@example.com: Your lab results are ready [results-ana.pdf]
Email to ben@example.com: Your lab results are ready [results-ben.pdf]
Lab results sent: 2

And the old mistake can't happen quietly any more. Handing LabResults the SMS channel fails in every language:

Language

LabResults given the SMS channel

Java

A compile error: "SmsChannel cannot be converted to FileSender"

C++

A compile error: no LabResults constructor takes an SmsChannel

JavaScript

A TypeError at the first send: sendFile is not a function

Python

An AttributeError at the first send: no attribute send_file

4 · The principle

No client depends on methods it doesn't use

The interface segregation principle says no code should be forced to depend on methods it doesn't use. Instead of one wide interface, a design offers small role interfaces, each shaped by what one kind of client needs. A class can play several roles, as EmailChannel does, and each client sees only the role it asked for.

«interface»TextSender+ send(String, String)«interface»FileSender+ sendFile(String, String, String)SmsChannel+ send(String, String)EmailChannel+ send(String, String)+ sendFile(String, String, String)«record»Patient− name: String− channel: TextSender− address: String− email: StringReminders~ remind(Patient, String)LabResults− sender: FileSender− sent: int~ sendResults(Patient, String)~ sent(): int

Two roles: Reminders reaches channels through TextSender, and LabResults through FileSender. EmailChannel plays both.

A wide interface does harm on both of its sides:

Side

The harm

In this story

Implementers

Methods they can't support, left empty or made to throw

SmsChannel.sendFile() did nothing, and the results were lost

Clients

Ties to methods they never call: rebuilt, retested and faked when those change

A fake for Reminders had to implement sendFile()

The client side is the one the principle is named for, and it grows with the codebase. In C++, every client that includes the interface's header is rebuilt when any method in it changes, used or not, and every test fake implements methods the test never touches.

5 · When breaking it is fine

Wide is fine when every caller uses all of it

Splitting has costs:

  • More types to name, find and keep in step.
  • A class's abilities are spread out. To see everything EmailChannel can do, you read two interfaces.
  • A client that needs several roles takes several parameters, or a combined interface made for it.

Keep one interface when:

  • Every client uses the methods together. A stack's push() and pop() go together for every caller; splitting them helps no one.
  • Every implementer supports all of it. An interface with no empty or refused methods anywhere isn't hurting its implementers.
  • There's one client. An interface made for a single caller is already shaped by it.

Split when

Stop when

An implementer leaves methods empty or makes them throw

Each interface has one method and one implementer

Clients use separate groups of methods that never meet

Every client needs all the roles together

A change to one method reaches clients that never call it

A reader opens five interfaces to learn what one class does

6 · Getting it right

Name the roles after what clients need

  • Start from the callers. List what each client calls; groups of methods that no client uses together are separate roles.
  • Name a role by what it lets a client do, such as TextSender and FileSender, not by the class that first implemented it.
  • Let a class play several roles, and combine roles for a client that needs both, with an interface that extends the two.

Two habits keep the roles small as the code grows:

  • Ask for the narrowest type. LabResults takes a FileSender, not an EmailChannel, so a secure patient portal that only carries files could serve it later.
  • Wrap a wide interface you don't own. For a third-party client with fifty methods, write a small role interface of your own and one class that adapts it.

Each language expresses a role differently:

Language

A role

Playing several roles

Java

An interface

A class implements as many interfaces as it likes

C++

An abstract class with only pure virtual functions

Multiple inheritance from several such classes

JavaScript

No interfaces: the methods a client calls are its role, written down in a comment

Any object with the methods fits

Python

A Protocol

A class fits every protocol whose methods it has, without naming them

7 · Look-alikes

Splitting interfaces, splitting classes

Several ideas split things apart. What tells them apart is what they split, and why.

Idea

The question it asks

Interface segregation

Does a caller depend on methods it doesn't use?

Single responsibility

Does a class have more than one reason to change?

Liskov substitution

Can every implementation keep the interface's promises? Wide interfaces force refusals

Cohesion

How closely do the parts of a module belong together?

Adapter

How does an existing class fit a client's interface? It can put a small role in front of a wide one

Single responsibility and interface segregation are easy to mix up. Single responsibility splits a class by its reasons to change; interface segregation splits an interface by its clients. EmailChannel has one responsibility, sending email, and still plays two roles, because two kinds of client use it in two different ways.

8 · In practice

Pricing at the garage

In the parking-lot practice problem, pricing is a contract with one method: a strategy returns the fee for a stay. Every pricing scheme can answer that, so no scheme is left with a method it can't support.

Keep it that way. If a draft adds methods that only one scheme supports, such as a monthly pass, the other schemes are left with empty bodies, and the checks report each one as leaving some of the interface's methods empty. Design the parking lot with interfaces shaped by their callers.

Check yourself

4 questions

1. The clinic adds a secure patient portal that can deliver files but not text messages. What should it implement?
2. Why was the empty SmsChannel.sendFile() worse than it looks?
3. Reminders only ever calls send(). What did the wide Channel cost it?
4. Which interface most needs splitting?

Takeaways

Remember this

  • No client should depend on methods it doesn't use: shape interfaces around the code that calls them.
  • A wide interface forces implementers to leave methods empty or make them throw, and an empty method fails silently.
  • Small role interfaces let a class implement only what it supports, and let the compiler stop a misfit.
  • A class can play several roles, and each client sees only the role it asked for.
  • Split by clients, not by method count: an interface every caller uses as a whole can stay wide.