# Interface Segregation Principle (ISP) > "No client should be forced to depend on methods it does not use." — Robert C. Martin ## Problem A single "fat" interface bundles many methods together. Some implementers only need a subset, so they are forced to provide dummy or `raise NotImplementedError` stubs for methods that are irrelevant to them. This creates: - **Misleading contracts** — callers think all methods are functional when they are not - **Fragile changes** — adding a method to the fat interface forces every implementer to change - **Tight coupling** — clients depend on capabilities they never invoke - **Testing burden** — mocks must implement unused methods **Classic smell:** An implementer contains methods that `pass`, `return nil`, or `throw UnsupportedOperationException`. ## Solution Break the fat interface into smaller, focused interfaces — each representing one cohesive capability. Implementers compose only the interfaces they actually need. Callers depend on only the narrow interface they require. ## When to Use - You see implementers with stub/no-op methods - Different callers use different subsets of the same interface - Adding a method to an interface forces unrelated implementers to change - You are designing a public API or SDK that many parties will implement ## When to Avoid - The interface is genuinely small (2-3 methods) and all implementers use every method - Splitting would create a swarm of single-method interfaces with no semantic meaning - The system is small and the coupling cost is negligible ## Pseudocode ### Before — fat interface forces irrelevant methods ``` INTERFACE Worker: METHOD work() METHOD eat() METHOD sleep() CLASS HumanWorker IMPLEMENTS Worker: METHOD work(): ... does work ... METHOD eat(): ... eats lunch ... METHOD sleep(): ... sleeps at night ... CLASS RobotWorker IMPLEMENTS Worker: METHOD work(): ... does work ... METHOD eat(): RAISE "Robots don't eat!" // Forced stub METHOD sleep(): RAISE "Robots don't sleep!" // Forced stub ``` ### After — segregated interfaces ``` INTERFACE Workable: METHOD work() INTERFACE Eatable: METHOD eat() INTERFACE Sleepable: METHOD sleep() CLASS HumanWorker IMPLEMENTS Workable, Eatable, Sleepable: METHOD work(): ... does work ... METHOD eat(): ... eats lunch ... METHOD sleep(): ... sleeps at night ... CLASS RobotWorker IMPLEMENTS Workable: METHOD work(): ... does work ... // No eat() or sleep() — not needed, not forced ``` ## Python ```python """ Interface Segregation Principle — Python BEFORE: A single Worker ABC forces Robot to implement eat() and sleep(). AFTER: Segregated ABCs let each class implement only what it needs. """ from abc import ABC, abstractmethod # ============================================================ # BEFORE: Violating ISP — fat interface # ============================================================ class WorkerBefore(ABC): @abstractmethod def work(self) -> str: ... @abstractmethod def eat(self) -> str: ... @abstractmethod def sleep(self) -> str: ... class HumanBefore(WorkerBefore): def __init__(self, name: str): self.name = name def work(self) -> str: return f"{self.name} is writing code" def eat(self) -> str: return f"{self.name} is eating lunch" def sleep(self) -> str: return f"{self.name} is sleeping" class RobotBefore(WorkerBefore): def __init__(self, model: str): self.model = model def work(self) -> str: return f"{self.model} is assembling parts" def eat(self) -> str: # Forced to implement — makes no sense for a robot raise NotImplementedError("Robots don't eat!") def sleep(self) -> str: # Forced to implement — makes no sense for a robot raise NotImplementedError("Robots don't sleep!") # ============================================================ # AFTER: Following ISP — segregated interfaces # ============================================================ class Workable(ABC): @abstractmethod def work(self) -> str: ... class Eatable(ABC): @abstractmethod def eat(self) -> str: ... class Sleepable(ABC): @abstractmethod def sleep(self) -> str: ... class Human(Workable, Eatable, Sleepable): def __init__(self, name: str): self.name = name def work(self) -> str: return f"{self.name} is writing code" def eat(self) -> str: return f"{self.name} is eating lunch" def sleep(self) -> str: return f"{self.name} is sleeping" class Robot(Workable): """Only implements Workable — no forced stubs.""" def __init__(self, model: str): self.model = model def work(self) -> str: return f"{self.model} is assembling parts" # A supervisor only cares about Workable — not eating or sleeping class Supervisor: def __init__(self, workers: list[Workable]): self.workers = workers def run_shift(self) -> None: for worker in self.workers: print(f" {worker.work()}") # A cafeteria only cares about Eatable class Cafeteria: def __init__(self, eaters: list[Eatable]): self.eaters = eaters def serve_lunch(self) -> None: for eater in self.eaters: print(f" {eater.eat()}") # ============================================================ # Demo # ============================================================ def main(): print("--- BEFORE (ISP violated) ---") human_old = HumanBefore("Alice") robot_old = RobotBefore("RX-78") print(f" {human_old.work()}") print(f" {human_old.eat()}") print(f" {robot_old.work()}") try: robot_old.eat() except NotImplementedError as e: print(f" ERROR: {e}") try: robot_old.sleep() except NotImplementedError as e: print(f" ERROR: {e}") print("\n--- AFTER (ISP applied) ---") alice = Human("Alice") bob = Human("Bob") rx78 = Robot("RX-78") t800 = Robot("T-800") print("Supervisor runs the shift (Workable only):") supervisor = Supervisor([alice, bob, rx78, t800]) supervisor.run_shift() print("\nCafeteria serves lunch (Eatable only):") cafeteria = Cafeteria([alice, bob]) # Robots NOT included — no stubs needed cafeteria.serve_lunch() if __name__ == "__main__": main() ``` ## Go ```go // Interface Segregation Principle — Go // // BEFORE: One large Worker interface forces Robot to implement Eat() and Sleep(). // AFTER: Small interfaces — Robot implements only what it needs. package main import "fmt" // ============================================================ // BEFORE: Violating ISP — fat interface // ============================================================ type WorkerBefore interface { Work() string Eat() string Sleep() string } type HumanBefore struct{ Name string } func (h HumanBefore) Work() string { return h.Name + " is writing code" } func (h HumanBefore) Eat() string { return h.Name + " is eating lunch" } func (h HumanBefore) Sleep() string { return h.Name + " is sleeping" } type RobotBefore struct{ Model string } func (r RobotBefore) Work() string { return r.Model + " is assembling parts" } func (r RobotBefore) Eat() string { return "ERROR: " + r.Model + " cannot eat!" } // Forced stub func (r RobotBefore) Sleep() string { return "ERROR: " + r.Model + " cannot sleep!" } // Forced stub // ============================================================ // AFTER: Following ISP — small, focused interfaces // ============================================================ type Workable interface { Work() string } type Eatable interface { Eat() string } type Sleepable interface { Sleep() string } type Human struct{ Name string } func (h Human) Work() string { return h.Name + " is writing code" } func (h Human) Eat() string { return h.Name + " is eating lunch" } func (h Human) Sleep() string { return h.Name + " is sleeping" } type Robot struct{ Model string } // Robot only implements Workable — no forced Eat or Sleep. func (r Robot) Work() string { return r.Model + " is assembling parts" } // RunShift only needs Workable — doesn't care about eating or sleeping. func RunShift(workers []Workable) { for _, w := range workers { fmt.Printf(" %s\n", w.Work()) } } // ServeLunch only needs Eatable. func ServeLunch(eaters []Eatable) { for _, e := range eaters { fmt.Printf(" %s\n", e.Eat()) } } // ============================================================ // Demo // ============================================================ func main() { fmt.Println("--- BEFORE (ISP violated) ---") hOld := HumanBefore{Name: "Alice"} rOld := RobotBefore{Model: "RX-78"} fmt.Printf(" %s\n", hOld.Work()) fmt.Printf(" %s\n", hOld.Eat()) fmt.Printf(" %s\n", rOld.Work()) fmt.Printf(" %s\n", rOld.Eat()) // Forced — meaningless fmt.Printf(" %s\n", rOld.Sleep()) // Forced — meaningless fmt.Println("\n--- AFTER (ISP applied) ---") alice := Human{Name: "Alice"} bob := Human{Name: "Bob"} rx78 := Robot{Model: "RX-78"} t800 := Robot{Model: "T-800"} fmt.Println("Supervisor runs the shift (Workable only):") RunShift([]Workable{alice, bob, rx78, t800}) fmt.Println("\nCafeteria serves lunch (Eatable only):") ServeLunch([]Eatable{alice, bob}) // Robots not included — no stubs needed } ``` ## JavaScript ```javascript // Interface Segregation Principle — JavaScript // // BEFORE: A single MultiFunctionDevice class forces SimplePrinter to implement scan/fax. // AFTER: Focused mixins — each device implements only the capabilities it has. // ============================================================ // BEFORE: Violating ISP — fat "interface" // ============================================================ class MultiFunctionDeviceBefore { print(doc) { throw new Error("Not implemented"); } scan(doc) { throw new Error("Not implemented"); } fax(doc) { throw new Error("Not implemented"); } } class AllInOnePrinterBefore extends MultiFunctionDeviceBefore { print(doc) { return `Printing: ${doc}`; } scan(doc) { return `Scanning: ${doc}`; } fax(doc) { return `Faxing: ${doc}`; } } class SimplePrinterBefore extends MultiFunctionDeviceBefore { print(doc) { return `Printing: ${doc}`; } // Forced to have scan and fax — they just throw errors } // ============================================================ // AFTER: Following ISP — focused capability classes // ============================================================ class Printer { print(doc) { throw new Error("Not implemented"); } } class Scanner { scan(doc) { throw new Error("Not implemented"); } } class Faxer { fax(doc) { throw new Error("Not implemented"); } } // Helper: simple mixin function (JS doesn't have multiple inheritance) function mixin(target, ...sources) { for (const source of sources) { const descriptors = Object.getOwnPropertyDescriptors(source.prototype); delete descriptors.constructor; Object.defineProperties(target.prototype, descriptors); } } // AllInOnePrinter has print + scan + fax class AllInOnePrinter { print(doc) { return `[AllInOne] Printing: ${doc}`; } scan(doc) { return `[AllInOne] Scanning: ${doc}`; } fax(doc) { return `[AllInOne] Faxing: ${doc}`; } } // SimplePrinter has only print — no scan/fax stubs needed class SimplePrinter { print(doc) { return `[Simple] Printing: ${doc}`; } } // ScannerOnly has only scan class ScannerOnly { scan(doc) { return `[Scanner] Scanning: ${doc}`; } } // Functions depend on narrow capabilities, not a fat interface: /** @param {{ print: (doc: string) => string }} printer */ function printJob(printer, doc) { console.log(` ${printer.print(doc)}`); } /** @param {{ scan: (doc: string) => string }} scanner */ function scanJob(scanner, doc) { console.log(` ${scanner.scan(doc)}`); } /** @param {{ fax: (doc: string) => string }} faxer */ function faxJob(faxer, doc) { console.log(` ${faxer.fax(doc)}`); } // ============================================================ // Demo // ============================================================ function main() { console.log("--- BEFORE (ISP violated) ---"); const allOld = new AllInOnePrinterBefore(); const simpleOld = new SimplePrinterBefore(); console.log(` ${allOld.print("report.pdf")}`); console.log(` ${allOld.scan("photo.jpg")}`); console.log(` ${simpleOld.print("letter.pdf")}`); try { simpleOld.scan("photo.jpg"); } catch (e) { console.log(` ERROR: ${e.message} (SimplePrinter forced to have scan)`); } try { simpleOld.fax("contract.pdf"); } catch (e) { console.log(` ERROR: ${e.message} (SimplePrinter forced to have fax)`); } console.log("\n--- AFTER (ISP applied) ---"); const allInOne = new AllInOnePrinter(); const simple = new SimplePrinter(); const scanner = new ScannerOnly(); console.log("Print jobs (only need print capability):"); printJob(allInOne, "report.pdf"); printJob(simple, "letter.pdf"); console.log("\nScan jobs (only need scan capability):"); scanJob(allInOne, "photo.jpg"); scanJob(scanner, "document.pdf"); console.log("\nFax jobs (only need fax capability):"); faxJob(allInOne, "contract.pdf"); // simple and scanner are NOT passed here — they don't have fax, and that's fine } main(); ``` ## Related Patterns - **Facade** — after segregating interfaces, a facade can aggregate multiple small interfaces into a convenient higher-level API. - **DIP** — small, focused interfaces are easier to invert and inject; ISP and DIP work well together. - **Adapter** — when a client is forced to use a fat interface, an adapter can expose only the relevant subset.