Skip to main content

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.

// One interface modelled on the single provider that
// happened to support everything.
interface CloudProvider is
method storeFile(name)
method getFile(name)
method createServer(region)
method listServers(region)
method getCDNAddress()
 
class Amazon implements CloudProvider is
method storeFile(name) is
// real implementation
method getFile(name) is
// real implementation
method createServer(region) is
// real implementation
method listServers(region) is
// real implementation
method getCDNAddress() is
// real implementation
 
// The second provider has no CDN and no servers,
// so it fills the gaps with lies.
class Dropbox implements CloudProvider is
method storeFile(name) is
// real implementation
method getFile(name) is
// real implementation
method createServer(region) is
throw new NotSupportedError()
method listServers(region) is
return [] // shrug
method getCDNAddress() is
return null // hope nobody calls this
«interface»CloudProvider+ storeFile(name)+ getFile(name)+ createServer(region)+ listServers(region)+ getCDNAddress()Amazon...+ storeFile(name)+ getFile(name)+ createServer(region)+ listServers(region)+ getCDNAddress()Dropbox...+ storeFile(name)+ getFile(name)+ createServer(region)+ listServers(region)+ getCDNAddress()not implemented
implements
BEFORE: one bloated interface, and a client that cannot honestly satisfy it.
«interface»CloudHostingProvider+ createServer(region)+ listServers(region)«interface»CDNProvider+ getCDNAddress()«interface»CloudStorageProvider+ storeFile(name)+ getFile(name)Amazon...+ storeFile(name)+ getFile(name)+ createServer(region)+ listServers(region)+ getCDNAddress()Dropbox...+ storeFile(name)+ getFile(name)
implements
AFTER: three narrow interfaces, and each provider claims only what it does.

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.

// One interface modelled on the human employees it was
// first written for.
interface Worker is
method work()
method eat()
method sleep()
 
class HumanWorker implements Worker is
method work() is
// does the job
method eat() is
// takes a lunch break
method sleep() is
// goes home at the end of the shift
 
// The warehouse adds a robot. Robots do not eat or sleep,
// but the interface insists they must claim to.
class RobotWorker implements Worker is
method work() is
// does the job, tirelessly
method eat() is
throw new NotSupportedError()
method sleep() is
throw new NotSupportedError()

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

Question 1 of 4

Why is stubbing out the unsupported methods a bad fix for a fat interface?