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.
Two kinds of message, one interface
Channel has send() and sendFile(). Reminders sends texts, and LabResults sends files, through each patient's channel.
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.
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());
}
}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:
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 |
|---|---|---|
|
|
|
|
| Any |
| To send text | A |
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.
interface TextSender {
void send(String to, String text);
}
interface FileSender {
void sendFile(String to, String text, String file);
}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 |
|
|---|---|
Java | A compile error: "SmsChannel cannot be converted to FileSender" |
C++ | A compile error: no |
JavaScript | A |
Python | An |
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.
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 |
|
Clients | Ties to methods they never call: rebuilt, retested and faked when those change | A fake for |
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
EmailChannelcan 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()andpop()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
TextSenderandFileSender, 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.
LabResultstakes aFileSender, not anEmailChannel, 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 | 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
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.