Interface segregation principle
Clients should not be forced to depend on methods they do not use.
Keep interfaces narrow enough that the classes implementing them never have to invent behavior they do not have. A "fat" interface is one that bundles several unrelated capabilities, and its damage is contagious: change it, and even clients that never touched the changed methods have to be recompiled, retested, and possibly rewritten.
Formal statement
No client should be forced to depend on methods it does not use. "Client" here means any code holding a reference typed as the interface - and the harm is not abstract: a client typed against a fat interface is coupled to every method on it, so a change to a method the client never calls can still force that client to recompile, and a mock or fake built for testing has to implement methods it will never exercise. The structural argument is short. A class may extend exactly one superclass, but it can implement as many interfaces as it likes. There is no scarcity to ration, so there is no excuse for cramming unrelated methods into a single declaration. Break it into refined pieces and let the classes that genuinely do everything implement all of them.
The example
You build a library that makes integrating with cloud providers painless. Version one
supports Amazon, and since Amazon offers essentially every cloud service in existence, your
CloudProvider interface covers storage, servers and CDN addresses. It fits perfectly,
because it was traced around exactly one shape.
Then you add a second provider. It does storage and nothing else. Suddenly most of the interface is too wide, and the only way to satisfy the compiler is to implement methods that cannot work: throw here, return an empty list there, return null and look away. Every one of those is a promise the type system is making on your behalf and your object cannot keep.
Split the interface into CloudHostingProvider, CDNProvider and CloudStorageProvider.
Amazon implements all three. The storage-only provider implements one, honestly. Client code
that only backs up files asks for a CloudStorageProvider, and the mismatch that used to be
a 3am exception becomes a compile error.
A second example
A Worker interface declares work(), eat() and sleep() because it was written to model
human employees, and humans do all three. Add a RobotWorker and two of those three methods
become fiction - a robot does not eat lunch or clock out to sleep, so RobotWorker either
throws or silently no-ops, and either way the interface is lying about what a robot can do.
Splitting into Workable, Feedable and Restable lets a scheduler that only ever assigns
work depend on Workable alone. It never needs to know robots exist, and RobotWorker never
needs to fake being human.
The smell
An implementer writing an empty method body, a method that returns null/None where the
interface promises a real value, or a method whose entire implementation is throwing
"not supported." Each is a class saying, in the only language a type system gives it, "this
part of the contract does not apply to me" - which means the contract was too wide before the
class ever showed up.
Patterns that lean on it
Adapter is often the fix once a fat interface already has real clients depending on it: rather than reshaping the interface itself (which breaks every existing implementer), an adapter narrows a wide interface down to the slice a particular client actually needs, without touching the original. It treats the symptom at the boundary instead of the cause at the source, which is the right call when the source is a third-party library you cannot edit.
Where it goes wrong
Like every principle here, this one can be taken past the point of usefulness.
- Do not split an interface that is already specific. The failure this principle prevents is an implementer forced to fake behavior. If nobody is faking anything, there is nothing to fix.
- Interface count is a real cost. Each one is a name to invent, a file to open, and an indirection to follow. Ten single-method interfaces where two would do makes the code more granular and less comprehensible at the same time.
- Do not split by aesthetics. Split along the lines that actual implementers and actual clients care about. If every implementer implements all the pieces and every client depends on all the pieces, you have distributed one interface across several files and gained nothing.
The trigger to watch for is a stub. The first throw new NotSupportedError() written to
satisfy an interface is the principle knocking. The honest cost on the other side: every
split interface is one more type name a new contributor has to learn before they can say
"this class does X" - pay it only where an implementer actually needed to fake something.
Try it yourself: Break CloudProvider into narrower interfaces so Dropbox never has to
implement createServer(), listServers(), or getCDNAddress(), then rewrite Dropbox to
implement only the interface it can honestly support.
Check yourself
Why is stubbing out the unsupported methods a bad fix for a fat interface?