bigonotes
CS ConceptsSOLID Principles

Interface Segregation Principle

Why many small, focused interfaces beat one fat interface, and how to split one so classes implement only what they use.

3 min readLast updated SOLID Principles

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:

  1. Classes implement only what they actually support.
  2. No dummy or throwing implementations.
  3. Each class's capabilities are obvious from its declaration.
  4. 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 pass signals 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.

Found a mistake? Tell me on LinkedIn ↗

Further reading#