Concurrency, the Repository pattern for persistence, REST API design over the domain, and advanced static analysis with mypy/Pyright.
Module 8: OOD in Practice
SOLID principles, the complete domain model, refactoring anti-patterns, and testing strategy — putting it all together.
SOLID Principles
SOLID is a mnemonic for five principles that, applied together, produce code that is easy to understand, extend, and test. All five have been applied throughout this series; here we name and systematise them.
S — Single Responsibility Principle
A class should have one reason to change. Book handles data and validation. Order handles lifecycle. Payment handles transactions. Each changes for its own reason.
O — Open/Closed Principle
Open for extension, closed for modification. Adding AudioBook only requires a new subclass — no existing code changes:
# Open for extension: add AudioBook without touching existing code
class AudioBook(Book):
def __init__(self, isbn, title, author, price, narrator, duration_hrs):
super().__init__(isbn, title, author, price)
self.narrator=narrator; self.duration_hrs=duration_hrs
def get_details(self) -> str:
return f"{self.title} by {self.author} | Audiobook ({self.duration_hrs}h) £{self.price:.2f}"
# No changes needed to:
# BookFactory — just add "audio" case
# Inventory — aggregates Book[*], already compatible
# ShoppingCart — aggregates Book[*], already compatible
# print loop — polymorphism handles it automaticallyL — Liskov Substitution Principle
Subtypes must be usable wherever the parent type is expected, without breaking the program:
# LSP test: this function must work correctly with ANY Book subtype
def apply_sale(books: list[Book], pct: float) -> None:
for b in books:
b.apply_discount(pct) # contract: reduces price by pct%
assert b.price >= 0 # invariant: price must be non-negative
assert b.get_discount_rate()==pct # postcondition
# DigitalBook PASSES: honours all Book contracts
# PhysicalBook PASSES: honours all Book contracts
# BadBook that ignores apply_discount() VIOLATES LSPI — Interface Segregation Principle
Prefer many small interfaces over one large one. We have Payable (2 methods), Discountable (2 methods), Serialisable (2 methods) — not one 20-method IBook. A payment processor depends only on Payable, never sees discount or serialisation methods.
D — Dependency Inversion Principle
Depend on abstractions, not concretions. Inject dependencies through constructors:
# DIP: inject abstractions, not concrete classes
class Order:
def __init__(self, order_id: str, customer: Customer,
pricing: PricingStrategy | None = None,
inventory: Inventory | None = None):
self._pricing = pricing or StandardPricing()
self._inventory = inventory or Inventory.get_instance()
# Order depends on PricingStrategy (abstract), not StandardPricing
# In tests: inject MockPricing() and InMemoryInventory()The Complete Domain Model
All eight modules integrated into one coherent model:
Reading top to bottom: Book is the abstract centre. DigitalBook and PhysicalBook specialise it. Customer and PremiumCustomer form a second hierarchy. ShoppingCart aggregates Books. Order composes OrderItem and Payment (which implements Payable). Inventory aggregates Books and notifies StockObservers. Order uses a PricingStrategy.
Refactoring Anti-Patterns
| Anti-pattern | Symptom | Refactoring |
|---|---|---|
| God Class | One class with 50+ methods spanning multiple concerns. | Extract to separate classes by responsibility (SRP). |
| Primitive Obsession | ISBN as raw str, price as float with no validation scattered everywhere. | Introduce value objects: ISBN, Money(amount, currency). |
| Shotgun Surgery | Adding one feature requires changing 10 different classes. | Move related behaviour into one class (OCP, DIP). |
| Feature Envy | A method in Order constantly calls methods on Customer. | Move the method to Customer (SRP). |
| Magic if/elif Chains | if book_type=="digital"... elif book_type=="physical"... | Replace with polymorphism or Factory Method (OCP). |
Testing OOD Code
Unit tests, contract tests (LSP enforcement), and integration tests with mock observers:
import pytest
from unittest.mock import MagicMock
# Unit test: test one class in isolation
class TestBook:
def setup_method(self):
self.book = DigitalBook("978","Clean Code","Martin",29.99,"PDF",5.2,"url")
def test_apply_discount(self):
self.book.apply_discount(10)
assert self.book.price == pytest.approx(26.99)
assert self.book.get_discount_rate() == 10
def test_discount_out_of_range(self):
with pytest.raises(ValueError): self.book.apply_discount(150)
# Contract test mixin: any Book implementation must pass these
class BookContractTests:
book: Book # set in subclass setup_method
def test_get_details_non_empty(self):
assert len(self.book.get_details()) > 0
def test_price_non_negative(self):
assert self.book.price >= 0
def test_apply_discount_contract(self):
old = self.book.price
self.book.apply_discount(20)
assert self.book.price == pytest.approx(old * 0.8)
class TestDigitalBookContracts(BookContractTests):
def setup_method(self):
self.book = DigitalBook("978","T","A",30.0,"EPUB",3.0,"url")
class TestPhysicalBookContracts(BookContractTests):
def setup_method(self):
self.book = PhysicalBook("978","T","A",30.0,0.5,"A4","WH1")
# Integration test with mock Observer
class TestInventoryObserver:
def test_notifies_on_stock_change(self):
inv = Inventory(); inv._observers.clear()
observer = MagicMock()
inv.subscribe(observer)
book = DigitalBook("978","Clean Code","Martin",29.99,"PDF",5.2,"url")
book.stock=10; inv.add_book(book)
inv.update_stock("978", -3)
observer.on_stock_change.assert_called_once_with("978", 7)Series Retrospective
What you have built across the eight modules:
| Module | Concept | Applied to |
|---|---|---|
| 1 | Objects & Classes | Book, Customer, ShoppingCart |
| 2 | Encapsulation | Private attrs, @property, validation |
| 3 | Inheritance | DigitalBook, PhysicalBook, PremiumCustomer |
| 4 | Polymorphism | get_details(), dunder methods, duck typing |
| 5 | Composition & Aggregation | Order◆OrderItem, Cart◇Book |
| 6 | Interfaces & Contracts | Payable, Discountable, Protocol |
| 7 | Design Patterns | Factory, Singleton, Observer, Strategy |
| 8 | SOLID & Practice | Full integration, testing, refactoring |
Summary
Each class has one reason to change. Group related behaviour; separate unrelated concerns.
Extend by adding new classes, not modifying existing ones. Polymorphism and Strategy are the mechanisms.
Every subtype must honour the parent contract. Contract tests enforce this mechanically.
Prefer many small interfaces. Clients depend only on what they use.
Depend on abstractions. Inject through constructors. Enables testing with mock implementations.
The BookContractTests mixin is the direct mechanical enforcement of LSP.