Interface segregation principle
The restaurant app had one Staff interface: cook, serve, deliver. When delivery
riders were added, Rider implements Staff needed all three, so two of them were written
as throw new Error('Riders do not cook'). Then someone added cleanTable to Staff.
Eleven classes stopped compiling. Nine of them got one more method that throws.
Nobody made a wrong decision that day. The interface was wrong from the start, and every class that touched it paid for it.
A Staff interface has cook(), serve() and deliver(). You have chefs, waiters, riders, and an owner who does all three on a busy day. How would you break the interface up, and what would the dispatch function that assigns deliveries ask for?
The idea
It has 47 buttons. Your grandmother uses four: power, volume up, volume down, and the one for her serial. The other 43 are things to press by mistake and then call you about. What she needs is a remote with four buttons. The TV can still support all 47 for the person who wants them.
The principle is usually stated from the caller’s side: a client should not be forced to depend on methods it does not use. It cuts both ways. The class implementing a fat interface writes methods it has no business having. And the function receiving one can see, and can call, far more than it needs.
Before: one interface for everyone
interface Staff {
cook(dish: string): string
serve(table: number): string
deliver(address: string): string
}
class Rider implements Staff {
cook(dish: string): string {
throw new Error('Riders do not cook')
}
serve(table: number): string {
throw new Error('Riders do not serve tables')
}
deliver(address: string): string {
return `Delivered to ${address}`
}
}
function dispatch(staff: Staff, address: string): string {
return staff.deliver(address)
}
console.log(dispatch(new Rider(), 'HSR Layout')) // Delivered to HSR LayoutTwo problems, one on each side of the interface. Rider has two methods that exist only
to throw, which is the Liskov violation from the
last page. And dispatch asks for a whole Staff when it calls one method, so nothing
stops the next developer from calling staff.cook() in there and finding out at runtime.
After: one interface per role
interface Cook {
cook(dish: string): string
}
interface Server {
serve(table: number): string
}
interface Courier {
deliver(address: string): string
}
class Chef implements Cook {
cook(dish: string): string {
return `Cooked ${dish}`
}
}
class Rider implements Courier {
deliver(address: string): string {
return `Delivered to ${address}`
}
}
class Owner implements Cook, Server, Courier {
cook(dish: string): string {
return `Owner cooked ${dish}`
}
serve(table: number): string {
return `Owner served table ${table}`
}
deliver(address: string): string {
return `Owner delivered to ${address}`
}
}
function dispatch(courier: Courier, address: string): string {
return courier.deliver(address)
}
console.log(dispatch(new Rider(), 'HSR Layout')) // Delivered to HSR Layout
console.log(dispatch(new Owner(), 'Koramangala'))
// > Owner delivered to Koramangala
dispatch(new Chef(), 'Indiranagar')
// Error: Argument of type 'Chef' is not assignable to parameter
// of type 'Courier'.Rider has one method and it is real. Owner implements three interfaces because the
owner really does three jobs. dispatch asks for a Courier, so inside it deliver is
the only method there is. And sending a chef out on a delivery is now a compile error.
Adding cleanTable later means a new Cleaner interface. Zero existing classes break.
Combining small interfaces in TypeScript
Small interfaces are easy to put back together when one function needs two roles. & is
the intersection type: a value that satisfies both.
interface Cook {
cook(dish: string): string
}
interface Server {
serve(table: number): string
}
function runCounter(person: Cook & Server): string {
return `${person.cook('dosa')}, then ${person.serve(4)}`
}
const owner = {
cook: (dish: string) => `cooked ${dish}`,
serve: (table: number) => `served table ${table}`,
}
console.log(runCounter(owner)) // cooked dosa, then served table 4You do not need a fourth interface called CookAndServer. The function states exactly
what it needs, in its own signature.
And when the fat interface belongs to a library you cannot change, Pick carves a small
one out of it:
interface Staff {
cook(dish: string): string
serve(table: number): string
deliver(address: string): string
}
type Courier = Pick<Staff, 'deliver'>
function dispatch(courier: Courier, address: string): string {
return courier.deliver(address)
}
const cyclist = { deliver: (address: string) => `Delivered to ${address}` }
console.log(dispatch(cyclist, 'HSR Layout')) // Delivered to HSR LayoutPick<Staff, 'deliver'> is a type with only the deliver member of Staff. Because
TypeScript is structural, a plain object with one method is enough to call dispatch.
Your function depends on one method even though the original interface has three.
Cookable, Servable, Deliverable, Payable, Printable, each with one method,
for a class that always uses them together. That is the same mistake in the other
direction. Split by role, meaning by who calls what. If every caller that uses read
also uses seek, they are one interface. The question to ask is “do I have a client
that needs only part of this?”
They sound alike, so expect the question. Single responsibility is about a class and
why it changes. Interface segregation is about a contract and who depends on it. A
class can have one responsibility and still be seen through several small interfaces by
different callers. A file store, for example, is one class that readers see as
Readable and writers see as Writable.
class Owner implements Cook, Server, Couriercourier: Courierperson: Cook & ServerPick<Staff, 'deliver'>Omit<Staff, 'cook'>{ deliver: (address: string) => ... }Checkpoint
1. Rider implements Staff and two of its three methods throw. Which two principles does this break at once?
2. dispatch only calls deliver(). Why does it matter whether its parameter is typed Staff or Courier?
3. A library gives you a 20 method interface and your function needs two of them. What is the TypeScript way to depend on only those two?
Interface segregation says no client should be forced to depend on methods it does not use, so I prefer several small role based interfaces to one large one. A fat interface hurts both sides. Implementers end up writing methods that throw or do nothing, and callers can see and call far more than they need, so a change to one method ripples everywhere. I split by role, like cook, server and courier, let a class implement as many as it really fulfils, and have each function ask for the narrowest one. In TypeScript I can combine small interfaces with an intersection type, or cut a small one out of a big one with Pick. I do not go as far as one interface per method. The test is whether some client needs only part of it.
