On this page
Description
The Interface Segregation Principle says no client should be forced to depend on methods it doesn't use. In practice it means many small, role-shaped interfaces instead of one large one - so implementations don't have to fake capabilities they lack, and callers depend only on what they actually call. This skill covers how to find the roles in a fat interface, how to split it, and when splitting goes too far.
Here's the kind of interface it's about:
Java
interface Machine {
void print(Document d);
void scan(Document d);
void fax(Document d);
}
class BasicPrinter implements Machine {
public void print(Document d) { /* ... */ }
public void scan(Document d) { throw new UnsupportedOperationException(); }
public void fax(Document d) { throw new UnsupportedOperationException(); }
}
Python
from abc import ABC, abstractmethod
class Machine(ABC):
@abstractmethod
def print(self, d: Document) -> None: ...
@abstractmethod
def scan(self, d: Document) -> None: ...
@abstractmethod
def fax(self, d: Document) -> None: ...
class BasicPrinter(Machine):
def print(self, d: Document) -> None: ...
def scan(self, d: Document) -> None:
raise NotImplementedError
def fax(self, d: Document) -> None:
raise NotImplementedError
BasicPrinter is forced to "implement" things it can't do. And every class that only prints still depends on Machine - so a change to the fax method signature recompiles (and re-tests) code that never faxes.
We'll go from simple to more technical:
- The principle, and role interfaces in Java and Python.
- Splitting a fat interface by how its clients use it, step by step.
- Where you'll see role interfaces in real libraries.
- When to stop splitting.
The principle
Clients should not be forced to depend on methods they do not use.
Split fat interfaces into small, role-based ones. Classes implement only the roles they can fulfill; clients depend only on the roles they need.
Python's duck typing makes this the default: a function that only calls print() accepts any object with a print() method. A typing.Protocol writes that role down for type checkers - and the implementations don't have to inherit from it.
Java
interface Printer { void print(Document d); }
interface Scanner { void scan(Document d); }
interface Fax { void fax(Document d); }
class BasicPrinter implements Printer { public void print(Document d) { ... } }
class OfficeMachine implements Printer, Scanner, Fax { ... }
class ReportService {
private final Printer printer; // needs printing only - and says so
ReportService(Printer printer) { this.printer = printer; }
}
Python
from typing import Protocol
class Printer(Protocol):
def print(self, d: Document) -> None: ...
class Scanner(Protocol):
def scan(self, d: Document) -> None: ...
class Fax(Protocol):
def fax(self, d: Document) -> None: ...
class BasicPrinter: # satisfies Printer just by having print()
def print(self, d: Document) -> None: ...
class OfficeMachine: # satisfies all three
def print(self, d: Document) -> None: ...
def scan(self, d: Document) -> None: ...
def fax(self, d: Document) -> None: ...
class ReportService:
def __init__(self, printer: Printer): # needs printing only - and says so
self._printer = printer
How to apply it, step by step
- Look at an interface's clients: which methods does each one actually call?
- Group methods by the role they serve for those clients.
- Extract one small interface per role.
- Let implementations implement several roles if they can; let clients ask for just the role they use.
- Keep a combined interface only if some clients genuinely need everything.
Where you'll see it
- The JDK's
Readable,Appendable,Closeable,AutoCloseable,Comparable- tiny capability interfaces. (Python:collections.abc.Iterable,Sized,Container,Hashable- and "file-like objects", which only need the methods you call.) - Repositories split into read and write roles (
OrderReader,OrderWriter), which also lets read-only services be handed a read-only view. - Spring Data offers
CrudRepositoryand smaller repository interfaces so you expose only the operations you want.
Which tool for which job?
| The situation | Do this |
|---|---|
| Implementations throw "not supported" for some methods | split the interface into roles they can each fulfill |
| A client calls two methods of a large interface | depend on a small role interface with just those two |
| A function needs two roles at once | a combined interface, or an intersection (<T extends Printer & Scanner>); in Python, a Protocol inheriting both |
| A fat third-party interface you can't change | define your own narrow role and adapt the third-party type to it |
| Some services must only read, never write | hand them a read-only role (OrderReader) |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
| One interface per method | every class implements a dozen interfaces; clients need eight of them | group methods by role |
| Splitting by guesswork, not by clients | roles nobody uses as a unit | split by which methods each client calls |
| Default methods that throw "not supported" | the fat interface, in disguise | real role interfaces |
| Role interfaces that mirror one class's public methods | "header interfaces" that change whenever the class does | interfaces shaped by what clients need |
Check yourself
Try to answer before opening each one - recalling beats re-reading.
1. A Robot implements Worker { work(); eat(); } and throws on eat(). What's the ISP fix?
Show answer
Split into role interfaces such as Workable and Eatable; the robot implements only Workable, humans implement both.
2. A reporting service only reads orders but depends on a OrderRepository with 25 methods. Why does ISP care, even if the repository supports them all?
Show answer
The service is coupled to 25 methods it doesn't use - their changes affect it, and any test double must fake all of them. Depend on a small OrderReader role instead.
3. Should every method get its own interface?
Show answer
No - group methods by the role they play for their clients. One-method interfaces are great for true capabilities, not as a blanket rule.
Key takeaways
- No class should implement - or depend on - methods it doesn't use.
- Split fat interfaces into role interfaces based on how clients use them.
- Classes can implement several roles; clients ask for exactly the role they need.
- Avoid interface confetti; segregate by role, not by method.