Solid principles skill

The SOLID principles are five design principles for managing dependencies at the class and module level.

by wondelai·MIT license·★ 2,235 Stars on the repo·GitHub ↗

Use now

Files of Solid principles

wondelai/main1 file
solid-principles.md
Show the full text413 lines

SOLID Principles

The SOLID principles are five design principles for managing dependencies at the class and module level. They were assembled and named by Robert C. Martin in the early 2000s, drawing on decades of software engineering wisdom. In Clean Architecture, SOLID principles serve as the mid-level building blocks that make the Dependency Rule possible. Without SOLID, the concentric circles would leak and the boundaries would crumble.

This reference covers each principle with definitions, code examples, common violations, and practical application guidance.

Table of Contents

  1. SRP: The Single Responsibility Principle
  2. OCP: The Open-Closed Principle
  3. LSP: The Liskov Substitution Principle
  4. ISP: The Interface Segregation Principle
  5. DIP: The Dependency Inversion Principle

SRP: The Single Responsibility Principle

"A module should have one, and only one, reason to change."

More precisely: a module should be responsible to one, and only one, actor (a group of users or stakeholders who want the system to change in the same way).

Understanding SRP

SRP is commonly misunderstood as "a function should do one thing." That's a good principle for functions, but SRP operates at a higher level. SRP says that the module (class) should serve one actor -- one group of people who would request changes.

Classic Violation
class Employee:
    def calculate_pay(self) -> Money:
        # Serves the CFO / accounting department
        regular_hours = self._get_regular_hours()
        overtime = self._get_overtime_hours()
        return regular_hours * self.hourly_rate + overtime * self.hourly_rate * 1.5

    def report_hours(self) -> HoursReport:
        # Serves the COO / operations department
        return HoursReport(
            regular=self._get_regular_hours(),
            overtime=self._get_overtime_hours(),
        )

    def save(self) -> None:
        # Serves the CTO / database administrators
        db.execute("INSERT INTO employees ...", self._to_dict())

    def _get_regular_hours(self) -> float:
        # Shared by calculate_pay and report_hours -- dangerous coupling
        return min(self.hours_worked, 40)

    def _get_overtime_hours(self) -> float:
        return max(self.hours_worked - 40, 0)

The problem: Three actors (CFO, COO, CTO) all have reasons to change this class. When the CFO wants to change how overtime is calculated, the shared _get_regular_hours method might be modified in a way that breaks the COO's reports.

SRP-Compliant Design
class PayCalculator:
    """Serves the CFO / accounting"""
    def calculate_pay(self, employee_data: EmployeeData) -> Money:
        regular = min(employee_data.hours_worked, 40)
        overtime = max(employee_data.hours_worked - 40, 0)
        return regular * employee_data.rate + overtime * employee_data.rate * 1.5

class HoursReporter:
    """Serves the COO / operations"""
    def report_hours(self, employee_data: EmployeeData) -> HoursReport:
        return HoursReport(
            regular=min(employee_data.hours_worked, 40),
            overtime=max(employee_data.hours_worked - 40, 0),
        )

class EmployeeRepository:
    """Serves the CTO / database administration"""
    def save(self, employee_data: EmployeeData) -> None:
        self._db.execute("INSERT INTO employees ...", employee_data.to_dict())

Each class now serves one actor. Changes requested by the CFO only affect PayCalculator. The COO's changes only affect HoursReporter. They can evolve independently.

SRP Indicators
Indicator Likely Violation
Class has methods serving different departments/teams Multiple actors
"And" in the class name (OrderValidatorAndNotifier) Multiple responsibilities
Class changes frequently for unrelated reasons Multiple change drivers
Merge conflicts from unrelated feature branches Multiple actors modifying same class
Unit tests require many unrelated mocks Class does too many things

OCP: The Open-Closed Principle

"A software artifact should be open for extension but closed for modification."

You should be able to extend the behavior of a system without modifying existing code. New features are added by writing new code, not by changing old code.

The Strategy Pattern Approach
# Closed for modification -- this code doesn't change when new shipping methods are added
class OrderService:
    def __init__(self, shipping_strategy: ShippingStrategy):
        self._shipping = shipping_strategy

    def calculate_total(self, order: Order) -> Money:
        subtotal = order.subtotal()
        shipping = self._shipping.calculate(order)
        return subtotal + shipping

# Open for extension -- add new shipping methods without touching OrderService
class ShippingStrategy(ABC):
    @abstractmethod
    def calculate(self, order: Order) -> Money:
        pass

class StandardShipping(ShippingStrategy):
    def calculate(self, order: Order) -> Money:
        return Money("5.99")

class ExpressShipping(ShippingStrategy):
    def calculate(self, order: Order) -> Money:
        return Money("14.99")

# New shipping method -- no existing code modified
class FreeShippingOver50(ShippingStrategy):
    def calculate(self, order: Order) -> Money:
        return Money("0.00") if order.subtotal() >= Money("50.00") else Money("5.99")
Common OCP Violations
# VIOLATION: Adding a new payment method requires modifying this function
def process_payment(method: str, amount: Money) -> PaymentResult:
    if method == "credit_card":
        return charge_credit_card(amount)
    elif method == "paypal":
        return charge_paypal(amount)
    elif method == "apple_pay":  # New method = new elif = modification
        return charge_apple_pay(amount)

Fix with OCP:

class PaymentProcessor(ABC):
    @abstractmethod
    def process(self, amount: Money) -> PaymentResult:
        pass

class CreditCardProcessor(PaymentProcessor):
    def process(self, amount: Money) -> PaymentResult:
        return self._gateway.charge(amount)

# Adding Apple Pay = new class, no modification to existing code
class ApplePayProcessor(PaymentProcessor):
    def process(self, amount: Money) -> PaymentResult:
        return self._apple_client.charge(amount)
OCP in Clean Architecture

OCP is foundational to the concentric circles model. The inner circles (entities, use cases) are closed for modification. The outer circles (adapters, frameworks) are open for extension. You extend the system by adding new adapters, new controllers, new gateways -- not by modifying business rules.

LSP: The Liskov Substitution Principle

"Subtypes must be substitutable for their base types."

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.

The Classic Violation: Square/Rectangle
class Rectangle:
    def __init__(self, width: float, height: float):
        self._width = width
        self._height = height

    def set_width(self, w: float) -> None:
        self._width = w

    def set_height(self, h: float) -> None:
        self._height = h

    def area(self) -> float:
        return self._width * self._height

class Square(Rectangle):
    def set_width(self, w: float) -> None:
        self._width = w
        self._height = w  # Must keep square invariant

    def set_height(self, h: float) -> None:
        self._width = h  # Must keep square invariant
        self._height = h

The problem: Code that works correctly with Rectangle breaks with Square:

def test_area(rect: Rectangle):
    rect.set_width(5)
    rect.set_height(4)
    assert rect.area() == 20  # Fails for Square! Area is 16 because set_height changed width

Square is NOT substitutable for Rectangle. LSP is violated.

LSP in Practice
Violation Pattern Why It Breaks Fix
Subclass throws unexpected exceptions Callers don't handle exceptions they didn't expect from the base type Subclass should honor the base type's exception contract
Subclass ignores methods (no-op override) Callers rely on the method doing something The class hierarchy is wrong; use composition or a different abstraction
Subclass strengthens preconditions Callers that work with base type fail with subtype Subtypes may weaken preconditions, never strengthen them
Subclass weakens postconditions Callers expect guarantees the subtype doesn't provide Subtypes may strengthen postconditions, never weaken them
LSP and Interfaces in Clean Architecture

LSP applies to interfaces as well as inheritance hierarchies. When a Use Case depends on OrderRepository, every implementation (PostgresOrderRepository, MongoOrderRepository, InMemoryOrderRepository) must behave consistently:

  • save() must persist the entity (or fail with a defined exception)
  • find_by_id() must return the entity if it exists or None if not
  • No implementation should silently drop data, return stale data, or throw exceptions not defined in the interface contract

ISP: The Interface Segregation Principle

"No client should be forced to depend on methods it does not use."

Fat interfaces create unnecessary coupling. When a client depends on an interface with methods it doesn't use, it becomes vulnerable to changes in those unused methods.

Classic Violation
class MultiFunctionDevice(ABC):
    @abstractmethod
    def print_document(self, doc: Document) -> None: pass

    @abstractmethod
    def scan_document(self) -> Image: pass

    @abstractmethod
    def fax_document(self, doc: Document, number: str) -> None: pass

    @abstractmethod
    def staple_pages(self, pages: list[Page]) -> None: pass

# A simple printer must implement fax and staple -- methods it can't fulfill
class SimplePrinter(MultiFunctionDevice):
    def print_document(self, doc: Document) -> None:
        # Actually prints
        ...

    def scan_document(self) -> Image:
        raise NotSupportedError()  # ISP violation!

    def fax_document(self, doc: Document, number: str) -> None:
        raise NotSupportedError()  # ISP violation!

    def staple_pages(self, pages: list[Page]) -> None:
        raise NotSupportedError()  # ISP violation!
ISP-Compliant Design
class Printer(ABC):
    @abstractmethod
    def print_document(self, doc: Document) -> None: pass

class Scanner(ABC):
    @abstractmethod
    def scan_document(self) -> Image: pass

class FaxMachine(ABC):
    @abstractmethod
    def fax_document(self, doc: Document, number: str) -> None: pass

# Simple printer only implements what it can do
class SimplePrinter(Printer):
    def print_document(self, doc: Document) -> None:
        ...

# Multi-function device implements all relevant interfaces
class OfficePrinter(Printer, Scanner, FaxMachine):
    def print_document(self, doc: Document) -> None: ...
    def scan_document(self) -> Image: ...
    def fax_document(self, doc: Document, number: str) -> None: ...
ISP in Clean Architecture

ISP directly supports the Dependency Rule. Use Cases define narrow, focused input and output port interfaces. Each adapter implements only the interfaces it needs:

# Focused interfaces (ISP-compliant)
class OrderReader(ABC):
    @abstractmethod
    def find_by_id(self, order_id: str) -> Order | None: pass

class OrderWriter(ABC):
    @abstractmethod
    def save(self, order: Order) -> None: pass

class OrderSearcher(ABC):
    @abstractmethod
    def search(self, criteria: SearchCriteria) -> list[Order]: pass

# Use Case that only reads doesn't depend on write methods
class GetOrderDetailsInteractor:
    def __init__(self, reader: OrderReader):  # Only depends on reading
        self._reader = reader

DIP: The Dependency Inversion Principle

"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions."

DIP is the mechanism that makes the Dependency Rule work. It inverts the natural direction of source code dependencies so that the volatile, concrete, outer-circle code depends on the stable, abstract, inner-circle code.

Without DIP (Natural Dependencies)
OrderService --> PostgresDatabase
  (high-level)     (low-level)

The high-level policy (OrderService) depends on the low-level detail (PostgresDatabase). Changing the database means changing the service.

With DIP (Inverted Dependencies)
OrderService --> OrderRepository (interface)
                       ^
                       |
              PostgresOrderRepository

Both the high-level service and the low-level database adapter depend on the abstraction (OrderRepository). The abstraction is defined by the high-level module, not by the low-level module.

DIP Implementation Pattern
# HIGH-LEVEL MODULE defines the abstraction
class OrderRepository(ABC):
    """Defined in the Use Case circle. The high-level policy dictates what it needs."""
    @abstractmethod
    def save(self, order: Order) -> None: pass

    @abstractmethod
    def find_by_id(self, order_id: str) -> Order | None: pass

# HIGH-LEVEL MODULE uses the abstraction
class PlaceOrderInteractor:
    def __init__(self, repo: OrderRepository):  # Depends on abstraction
        self._repo = repo

    def execute(self, request: PlaceOrderRequest) -> None:
        order = Order.create(request.items, request.customer_id)
        self._repo.save(order)  # Calls abstraction

# LOW-LEVEL MODULE implements the abstraction
class PostgresOrderRepository(OrderRepository):  # Depends on abstraction
    def __init__(self, pool):
        self._pool = pool

    def save(self, order: Order) -> None:
        # SQL details here -- low-level
        ...

# COMPOSITION ROOT wires them together
def main():
    pool = create_pool(DATABASE_URL)
    repo = PostgresOrderRepository(pool)
    interactor = PlaceOrderInteractor(repo)  # Inject concrete into abstract slot
DIP: Who Owns the Interface?

This is the critical insight: the interface belongs to the high-level module, not the low-level module.

Ownership Meaning Result
Interface owned by high-level module The Use Case defines what it needs Low-level module adapts to high-level needs
Interface owned by low-level module The database defines its capabilities High-level module must adapt to database -- dependency NOT inverted

When the Use Case defines OrderRepository, it specifies methods like save(order) and find_by_id(id) -- business-oriented operations. The database adapter must conform to this business-oriented interface.

When the database adapter defines the interface, it specifies methods like execute_query(sql) and fetch_rows(table) -- technology-oriented operations. The Use Case must conform to the database's way of thinking. This is the natural dependency direction, NOT inverted.

Common DIP Violations
Violation Example Fix
Importing concrete classes in high-level modules from stripe import StripeClient in Use Case Define PaymentGateway interface in Use Case; implement with Stripe in adapter
Using static/global factory methods Database.get_instance() in Use Case Inject repository through constructor
Depending on framework types in domain @Autowired on domain class Use plain constructor injection; wire in Main
Low-level module defines the interface IStripeGateway lives in the Stripe adapter package Move interface to Use Case package; rename to PaymentGateway
New operator in high-level code repo = PostgresRepository() inside Use Case Inject through constructor; instantiate in Main
DIP and Clean Architecture

DIP is the engine of Clean Architecture. Every boundary in the concentric circles model is maintained through dependency inversion:

  • Use Case to Database: OrderRepository interface (defined by Use Case) inverts the dependency so the database adapter depends inward
  • Use Case to Web: PlaceOrderOutput interface (defined by Use Case) inverts the dependency so the presenter depends inward
  • Use Case to External Service: EmailService interface (defined by Use Case) inverts the dependency so the email adapter depends inward

Without DIP, inner circles would depend on outer circles, the Dependency Rule would be violated, and the architecture would collapse into a ball of mud.

1# SOLID Principles
2 
3The SOLID principles are five design principles for managing dependencies at the class and module level. They were assembled and named by Robert C. Martin in the early 2000s, drawing on decades of software engineering wisdom. In Clean Architecture, SOLID principles serve as the mid-level building blocks that make the Dependency Rule possible. Without SOLID, the concentric circles would leak and the boundaries would crumble.
4 
5This reference covers each principle with definitions, code examples, common violations, and practical application guidance.
6 
7 
8## Table of Contents
91. [SRP: The Single Responsibility Principle](#srp-the-single-responsibility-principle)
102. [OCP: The Open-Closed Principle](#ocp-the-open-closed-principle)
113. [LSP: The Liskov Substitution Principle](#lsp-the-liskov-substitution-principle)
124. [ISP: The Interface Segregation Principle](#isp-the-interface-segregation-principle)
135. [DIP: The Dependency Inversion Principle](#dip-the-dependency-inversion-principle)
14 
15---
16 
17## SRP: The Single Responsibility Principle
18 
19**"A module should have one, and only one, reason to change."**
20 
21More precisely: a module should be responsible to one, and only one, actor (a group of users or stakeholders who want the system to change in the same way).
22 
23### Understanding SRP
24 
25SRP is commonly misunderstood as "a function should do one thing." That's a good principle for functions, but SRP operates at a higher level. SRP says that the module (class) should serve one actor -- one group of people who would request changes.
26 
27### Classic Violation
28 
29```python
30class Employee:
31 def calculate_pay(self) -> Money:
32 # Serves the CFO / accounting department
33 regular_hours = self._get_regular_hours()
34 overtime = self._get_overtime_hours()
35 return regular_hours * self.hourly_rate + overtime * self.hourly_rate * 1.5
36 
37 def report_hours(self) -> HoursReport:
38 # Serves the COO / operations department
39 return HoursReport(
40 regular=self._get_regular_hours(),
41 overtime=self._get_overtime_hours(),
42 )
43 
44 def save(self) -> None:
45 # Serves the CTO / database administrators
46 db.execute("INSERT INTO employees ...", self._to_dict())
47 
48 def _get_regular_hours(self) -> float:
49 # Shared by calculate_pay and report_hours -- dangerous coupling
50 return min(self.hours_worked, 40)
51 
52 def _get_overtime_hours(self) -> float:
53 return max(self.hours_worked - 40, 0)
54```
55 
56**The problem:** Three actors (CFO, COO, CTO) all have reasons to change this class. When the CFO wants to change how overtime is calculated, the shared `_get_regular_hours` method might be modified in a way that breaks the COO's reports.
57 
58### SRP-Compliant Design
59 
60```python
61class PayCalculator:
62 """Serves the CFO / accounting"""
63 def calculate_pay(self, employee_data: EmployeeData) -> Money:
64 regular = min(employee_data.hours_worked, 40)
65 overtime = max(employee_data.hours_worked - 40, 0)
66 return regular * employee_data.rate + overtime * employee_data.rate * 1.5
67 
68class HoursReporter:
69 """Serves the COO / operations"""
70 def report_hours(self, employee_data: EmployeeData) -> HoursReport:
71 return HoursReport(
72 regular=min(employee_data.hours_worked, 40),
73 overtime=max(employee_data.hours_worked - 40, 0),
74 )
75 
76class EmployeeRepository:
77 """Serves the CTO / database administration"""
78 def save(self, employee_data: EmployeeData) -> None:
79 self._db.execute("INSERT INTO employees ...", employee_data.to_dict())
80```
81 
82Each class now serves one actor. Changes requested by the CFO only affect `PayCalculator`. The COO's changes only affect `HoursReporter`. They can evolve independently.
83 
84### SRP Indicators
85 
86| Indicator | Likely Violation |
87|-----------|-----------------|
88| Class has methods serving different departments/teams | Multiple actors |
89| "And" in the class name (`OrderValidatorAndNotifier`) | Multiple responsibilities |
90| Class changes frequently for unrelated reasons | Multiple change drivers |
91| Merge conflicts from unrelated feature branches | Multiple actors modifying same class |
92| Unit tests require many unrelated mocks | Class does too many things |
93 
94## OCP: The Open-Closed Principle
95 
96**"A software artifact should be open for extension but closed for modification."**
97 
98You should be able to extend the behavior of a system without modifying existing code. New features are added by writing new code, not by changing old code.
99 
100### The Strategy Pattern Approach
101 
102```python
103# Closed for modification -- this code doesn't change when new shipping methods are added
104class OrderService:
105 def __init__(self, shipping_strategy: ShippingStrategy):
106 self._shipping = shipping_strategy
107 
108 def calculate_total(self, order: Order) -> Money:
109 subtotal = order.subtotal()
110 shipping = self._shipping.calculate(order)
111 return subtotal + shipping
112 
113# Open for extension -- add new shipping methods without touching OrderService
114class ShippingStrategy(ABC):
115 @abstractmethod
116 def calculate(self, order: Order) -> Money:
117 pass
118 
119class StandardShipping(ShippingStrategy):
120 def calculate(self, order: Order) -> Money:
121 return Money("5.99")
122 
123class ExpressShipping(ShippingStrategy):
124 def calculate(self, order: Order) -> Money:
125 return Money("14.99")
126 
127# New shipping method -- no existing code modified
128class FreeShippingOver50(ShippingStrategy):
129 def calculate(self, order: Order) -> Money:
130 return Money("0.00") if order.subtotal() >= Money("50.00") else Money("5.99")
131```
132 
133### Common OCP Violations
134 
135```python
136# VIOLATION: Adding a new payment method requires modifying this function
137def process_payment(method: str, amount: Money) -> PaymentResult:
138 if method == "credit_card":
139 return charge_credit_card(amount)
140 elif method == "paypal":
141 return charge_paypal(amount)
142 elif method == "apple_pay": # New method = new elif = modification
143 return charge_apple_pay(amount)
144```
145 
146**Fix with OCP:**
147 
148```python
149class PaymentProcessor(ABC):
150 @abstractmethod
151 def process(self, amount: Money) -> PaymentResult:
152 pass
153 
154class CreditCardProcessor(PaymentProcessor):
155 def process(self, amount: Money) -> PaymentResult:
156 return self._gateway.charge(amount)
157 
158# Adding Apple Pay = new class, no modification to existing code
159class ApplePayProcessor(PaymentProcessor):
160 def process(self, amount: Money) -> PaymentResult:
161 return self._apple_client.charge(amount)
162```
163 
164### OCP in Clean Architecture
165 
166OCP is foundational to the concentric circles model. The inner circles (entities, use cases) are closed for modification. The outer circles (adapters, frameworks) are open for extension. You extend the system by adding new adapters, new controllers, new gateways -- not by modifying business rules.
167 
168## LSP: The Liskov Substitution Principle
169 
170**"Subtypes must be substitutable for their base types."**
171 
172If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.
173 
174### The Classic Violation: Square/Rectangle
175 
176```python
177class Rectangle:
178 def __init__(self, width: float, height: float):
179 self._width = width
180 self._height = height
181 
182 def set_width(self, w: float) -> None:
183 self._width = w
184 
185 def set_height(self, h: float) -> None:
186 self._height = h
187 
188 def area(self) -> float:
189 return self._width * self._height
190 
191class Square(Rectangle):
192 def set_width(self, w: float) -> None:
193 self._width = w
194 self._height = w # Must keep square invariant
195 
196 def set_height(self, h: float) -> None:
197 self._width = h # Must keep square invariant
198 self._height = h
199```
200 
201**The problem:** Code that works correctly with `Rectangle` breaks with `Square`:
202 
203```python
204def test_area(rect: Rectangle):
205 rect.set_width(5)
206 rect.set_height(4)
207 assert rect.area() == 20 # Fails for Square! Area is 16 because set_height changed width
208```
209 
210`Square` is NOT substitutable for `Rectangle`. LSP is violated.
211 
212### LSP in Practice
213 
214| Violation Pattern | Why It Breaks | Fix |
215|-------------------|--------------|-----|
216| Subclass throws unexpected exceptions | Callers don't handle exceptions they didn't expect from the base type | Subclass should honor the base type's exception contract |
217| Subclass ignores methods (no-op override) | Callers rely on the method doing something | The class hierarchy is wrong; use composition or a different abstraction |
218| Subclass strengthens preconditions | Callers that work with base type fail with subtype | Subtypes may weaken preconditions, never strengthen them |
219| Subclass weakens postconditions | Callers expect guarantees the subtype doesn't provide | Subtypes may strengthen postconditions, never weaken them |
220 
221### LSP and Interfaces in Clean Architecture
222 
223LSP applies to interfaces as well as inheritance hierarchies. When a Use Case depends on `OrderRepository`, every implementation (`PostgresOrderRepository`, `MongoOrderRepository`, `InMemoryOrderRepository`) must behave consistently:
224 
225- `save()` must persist the entity (or fail with a defined exception)
226- `find_by_id()` must return the entity if it exists or `None` if not
227- No implementation should silently drop data, return stale data, or throw exceptions not defined in the interface contract
228 
229## ISP: The Interface Segregation Principle
230 
231**"No client should be forced to depend on methods it does not use."**
232 
233Fat interfaces create unnecessary coupling. When a client depends on an interface with methods it doesn't use, it becomes vulnerable to changes in those unused methods.
234 
235### Classic Violation
236 
237```python
238class MultiFunctionDevice(ABC):
239 @abstractmethod
240 def print_document(self, doc: Document) -> None: pass
241 
242 @abstractmethod
243 def scan_document(self) -> Image: pass
244 
245 @abstractmethod
246 def fax_document(self, doc: Document, number: str) -> None: pass
247 
248 @abstractmethod
249 def staple_pages(self, pages: list[Page]) -> None: pass
250 
251# A simple printer must implement fax and staple -- methods it can't fulfill
252class SimplePrinter(MultiFunctionDevice):
253 def print_document(self, doc: Document) -> None:
254 # Actually prints
255 ...
256 
257 def scan_document(self) -> Image:
258 raise NotSupportedError() # ISP violation!
259 
260 def fax_document(self, doc: Document, number: str) -> None:
261 raise NotSupportedError() # ISP violation!
262 
263 def staple_pages(self, pages: list[Page]) -> None:
264 raise NotSupportedError() # ISP violation!
265```
266 
267### ISP-Compliant Design
268 
269```python
270class Printer(ABC):
271 @abstractmethod
272 def print_document(self, doc: Document) -> None: pass
273 
274class Scanner(ABC):
275 @abstractmethod
276 def scan_document(self) -> Image: pass
277 
278class FaxMachine(ABC):
279 @abstractmethod
280 def fax_document(self, doc: Document, number: str) -> None: pass
281 
282# Simple printer only implements what it can do
283class SimplePrinter(Printer):
284 def print_document(self, doc: Document) -> None:
285 ...
286 
287# Multi-function device implements all relevant interfaces
288class OfficePrinter(Printer, Scanner, FaxMachine):
289 def print_document(self, doc: Document) -> None: ...
290 def scan_document(self) -> Image: ...
291 def fax_document(self, doc: Document, number: str) -> None: ...
292```
293 
294### ISP in Clean Architecture
295 
296ISP directly supports the Dependency Rule. Use Cases define narrow, focused input and output port interfaces. Each adapter implements only the interfaces it needs:
297 
298```python
299# Focused interfaces (ISP-compliant)
300class OrderReader(ABC):
301 @abstractmethod
302 def find_by_id(self, order_id: str) -> Order | None: pass
303 
304class OrderWriter(ABC):
305 @abstractmethod
306 def save(self, order: Order) -> None: pass
307 
308class OrderSearcher(ABC):
309 @abstractmethod
310 def search(self, criteria: SearchCriteria) -> list[Order]: pass
311 
312# Use Case that only reads doesn't depend on write methods
313class GetOrderDetailsInteractor:
314 def __init__(self, reader: OrderReader): # Only depends on reading
315 self._reader = reader
316```
317 
318## DIP: The Dependency Inversion Principle
319 
320**"High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions."**
321 
322DIP is the mechanism that makes the Dependency Rule work. It inverts the natural direction of source code dependencies so that the volatile, concrete, outer-circle code depends on the stable, abstract, inner-circle code.
323 
324### Without DIP (Natural Dependencies)
325 
326```
327OrderService --> PostgresDatabase
328 (high-level) (low-level)
329```
330 
331The high-level policy (OrderService) depends on the low-level detail (PostgresDatabase). Changing the database means changing the service.
332 
333### With DIP (Inverted Dependencies)
334 
335```
336OrderService --> OrderRepository (interface)
337 ^
338 |
339 PostgresOrderRepository
340```
341 
342Both the high-level service and the low-level database adapter depend on the abstraction (OrderRepository). The abstraction is defined by the high-level module, not by the low-level module.
343 
344### DIP Implementation Pattern
345 
346```python
347# HIGH-LEVEL MODULE defines the abstraction
348class OrderRepository(ABC):
349 """Defined in the Use Case circle. The high-level policy dictates what it needs."""
350 @abstractmethod
351 def save(self, order: Order) -> None: pass
352 
353 @abstractmethod
354 def find_by_id(self, order_id: str) -> Order | None: pass
355 
356# HIGH-LEVEL MODULE uses the abstraction
357class PlaceOrderInteractor:
358 def __init__(self, repo: OrderRepository): # Depends on abstraction
359 self._repo = repo
360 
361 def execute(self, request: PlaceOrderRequest) -> None:
362 order = Order.create(request.items, request.customer_id)
363 self._repo.save(order) # Calls abstraction
364 
365# LOW-LEVEL MODULE implements the abstraction
366class PostgresOrderRepository(OrderRepository): # Depends on abstraction
367 def __init__(self, pool):
368 self._pool = pool
369 
370 def save(self, order: Order) -> None:
371 # SQL details here -- low-level
372 ...
373 
374# COMPOSITION ROOT wires them together
375def main():
376 pool = create_pool(DATABASE_URL)
377 repo = PostgresOrderRepository(pool)
378 interactor = PlaceOrderInteractor(repo) # Inject concrete into abstract slot
379```
380 
381### DIP: Who Owns the Interface?
382 
383This is the critical insight: **the interface belongs to the high-level module, not the low-level module.**
384 
385| Ownership | Meaning | Result |
386|-----------|---------|--------|
387| Interface owned by high-level module | The Use Case defines what it needs | Low-level module adapts to high-level needs |
388| Interface owned by low-level module | The database defines its capabilities | High-level module must adapt to database -- dependency NOT inverted |
389 
390When the Use Case defines `OrderRepository`, it specifies methods like `save(order)` and `find_by_id(id)` -- business-oriented operations. The database adapter must conform to this business-oriented interface.
391 
392When the database adapter defines the interface, it specifies methods like `execute_query(sql)` and `fetch_rows(table)` -- technology-oriented operations. The Use Case must conform to the database's way of thinking. This is the natural dependency direction, NOT inverted.
393 
394### Common DIP Violations
395 
396| Violation | Example | Fix |
397|-----------|---------|-----|
398| Importing concrete classes in high-level modules | `from stripe import StripeClient` in Use Case | Define `PaymentGateway` interface in Use Case; implement with Stripe in adapter |
399| Using static/global factory methods | `Database.get_instance()` in Use Case | Inject repository through constructor |
400| Depending on framework types in domain | `@Autowired` on domain class | Use plain constructor injection; wire in Main |
401| Low-level module defines the interface | `IStripeGateway` lives in the Stripe adapter package | Move interface to Use Case package; rename to `PaymentGateway` |
402| New operator in high-level code | `repo = PostgresRepository()` inside Use Case | Inject through constructor; instantiate in Main |
403 
404### DIP and Clean Architecture
405 
406DIP is the engine of Clean Architecture. Every boundary in the concentric circles model is maintained through dependency inversion:
407 
408- **Use Case to Database:** `OrderRepository` interface (defined by Use Case) inverts the dependency so the database adapter depends inward
409- **Use Case to Web:** `PlaceOrderOutput` interface (defined by Use Case) inverts the dependency so the presenter depends inward
410- **Use Case to External Service:** `EmailService` interface (defined by Use Case) inverts the dependency so the email adapter depends inward
411 
412Without DIP, inner circles would depend on outer circles, the Dependency Rule would be violated, and the architecture would collapse into a ball of mud.
413 

Discussion