d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
14 KiB
14 KiB
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
"""
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
// 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
// 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.