The Interface Segregation Principle (ISP) says don't force a class to implement methods it doesn't need. Prefer many small, focused interfaces over one large general one. Fat interfaces lead to dummy implementations that throw or do nothing, and every change to the big interface ripples into classes that never cared.
The idea#
Imagine every job application made you fill in a pilot license number, a medical degree, a teaching certificate and a truck license, even though you're applying to be a programmer. You'd write "N/A" everywhere. ISP says: only ask for what you actually need.
Violation: one big device interface#
One interface lists every feature a device might have, so a basic phone has to stub out most of it:
from abc import ABC, abstractmethod
class SmartDevice(ABC):
@abstractmethod
def call(self) -> None: ...
@abstractmethod
def send_text(self) -> None: ...
@abstractmethod
def browse_internet(self) -> None: ...
@abstractmethod
def take_photo(self) -> None: ...
@abstractmethod
def play_games(self) -> None: ...
@abstractmethod
def track_fitness(self) -> None: ...
# A basic phone can only call and text, but must implement all six
class BasicPhone(SmartDevice):
def call(self) -> None:
print("Calling")
def send_text(self) -> None:
print("Texting")
def browse_internet(self) -> None:
raise NotImplementedError
def take_photo(self) -> None:
raise NotImplementedError
def play_games(self) -> None:
raise NotImplementedError
def track_fitness(self) -> None:
raise NotImplementedError
BasicPhone().call() # works, but four methods are traps
Fix: split by capability#
Give each capability its own small interface, and let each device inherit only the ones it supports:
from abc import ABC, abstractmethod
class Caller(ABC):
@abstractmethod
def call(self) -> None: ...
class Texter(ABC):
@abstractmethod
def send_text(self) -> None: ...
class Camera(ABC):
@abstractmethod
def take_photo(self) -> None: ...
class Browser(ABC):
@abstractmethod
def browse_internet(self) -> None: ...
class Gaming(ABC):
@abstractmethod
def play_games(self) -> None: ...
class Smartphone(Caller, Texter, Camera, Browser, Gaming):
def call(self) -> None: print("Calling")
def send_text(self) -> None: print("Texting")
def take_photo(self) -> None: print("Click")
def browse_internet(self) -> None: print("Browsing")
def play_games(self) -> None: print("Playing")
class BasicPhone(Caller, Texter):
def call(self) -> None: print("Calling")
def send_text(self) -> None: print("Texting")
class SmartWatch(Caller, Browser):
def call(self) -> None: print("Calling from wrist")
def browse_internet(self) -> None: print("Tiny browsing")
for device in [Smartphone(), BasicPhone(), SmartWatch()]:
device.call()
Benefits:
- Classes implement only what they actually support.
- No dummy or throwing implementations.
- Each class's capabilities are obvious from its declaration.
- You can mix and match interfaces freely, and callers depend only on the slice they use.
It's like ordering à la carte instead of a giant combo meal you only half eat.
Practice: designing animals#
Should an Animal interface have fly(), swim(), run() and climb()? No: split by ability.
from abc import ABC, abstractmethod
class Flyer(ABC):
@abstractmethod
def fly(self) -> None: ...
class Swimmer(ABC):
@abstractmethod
def swim(self) -> None: ...
class Runner(ABC):
@abstractmethod
def run(self) -> None: ...
class Climber(ABC):
@abstractmethod
def climb(self) -> None: ...
class Duck(Flyer, Swimmer, Runner):
def fly(self) -> None: print("Flapping")
def swim(self) -> None: print("Paddling")
def run(self) -> None: print("Waddling")
class Fish(Swimmer):
def swim(self) -> None: print("Swimming")
class Monkey(Runner, Climber):
def run(self) -> None: print("Running")
def climb(self) -> None: print("Climbing")
Duck().fly()
Fish().swim()
Monkey().climb()
No fish pretending to climb, and no monkey forced to implement swim().
Trade-offs#
Segregation has a cost too:
- Don't split down to one method per interface by reflex. Group methods that are always used together.
- Splitting is most valuable where different clients use different subsets of a type.
Key takeaways#
- Many small interfaces beat one fat one.
- A method implemented only to throw or
passsignals an ISP violation. - Split interfaces by capability ("can call", "can swim") and let classes combine them.
- Clients should depend only on the methods they actually call.