Skip to main content
Clean Architecture 70 XP · 7 min

ISP: The Interface Segregation Principle

Don't depend on things you don't use — fat interfaces force unnecessary recompilation, coupling, and vulnerability to failures in unrelated code.

Showing
Ad (728×90)

Why this matters

The ISP is about avoiding unnecessary coupling through over-broad interfaces. In statically typed languages, depending on a module that contains things you don't use means your code must be recompiled whenever any of those unused things change. In dynamically typed languages, it means your deployment must happen whenever an unrelated part of the dependency is modified.

Martin extends ISP to the architectural level: depending on a component means depending on everything that component contains. If a component is split by ISP — each interface segregated so that clients only depend on what they call — then a bug in an unused feature of that component cannot force a redeployment of your service. The fat interface is ISP's central enemy: if some implementors must leave methods empty or raise errors for methods they don't support, the interface is doing too much and carrying baggage that poisons unrelated consumers.

The problem

A fat Worker interface forces Robot to implement eat() and sleep() with empty stubs — meaningless coupling to behavior it will never use.

Bad

from abc import ABC, abstractmethod

class Worker(ABC):
    @abstractmethod
    def work(self) -> None: ...

    @abstractmethod
    def eat(self) -> None: ...    # meaningless for robots

    @abstractmethod
    def sleep(self) -> None: ...  # meaningless for robots

class Robot(Worker):
    def work(self) -> None:  print("Processing...")
    def eat(self) -> None:   pass          # forced stub
    def sleep(self) -> None: pass          # forced stub
interface Worker {
  work(): void;
  eat(): void;    // robots don't eat
  sleep(): void;  // robots don't sleep
}

class Robot implements Worker {
  work(): void  { console.log("Processing..."); }
  eat(): void   { /* forced empty stub */ }
  sleep(): void { /* forced empty stub */ }
}

The solution

Three focused interfaces let each class depend only on the methods it actually uses. Robot implements Workable only — no empty stubs, no unnecessary coupling.

Good

from abc import ABC, abstractmethod

class Workable(ABC):
    @abstractmethod
    def work(self) -> None: ...

class Eatable(ABC):
    @abstractmethod
    def eat(self) -> None: ...

class Sleepable(ABC):
    @abstractmethod
    def sleep(self) -> None: ...

class HumanWorker(Workable, Eatable, Sleepable):
    def work(self) -> None:  print("Working...")
    def eat(self) -> None:   print("Eating...")
    def sleep(self) -> None: print("Sleeping...")

class Robot(Workable):         # depends only on what it uses
    def work(self) -> None:    print("Processing...")
interface Workable  { work(): void; }
interface Eatable   { eat(): void; }
interface Sleepable { sleep(): void; }

class HumanWorker implements Workable, Eatable, Sleepable {
  work(): void  { console.log("Working..."); }
  eat(): void   { console.log("Eating..."); }
  sleep(): void { console.log("Sleeping..."); }
}

class Robot implements Workable {   // depends only on what it uses
  work(): void { console.log("Processing..."); }
}

Key takeaway

An interface with methods that some implementors must leave empty is a design smell — it forces unnecessary coupling. Split it until every implementor genuinely needs every method it must provide.

Done with this lesson?

Mark it complete to earn XP and track your progress.