477 lines
14 KiB
Markdown
477 lines
14 KiB
Markdown
# 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.
|