Ran Wei /Object-Oriented Design/Module 8
中文
Object-Oriented Design — Ran Wei

Module 8: OOD in Practice

SOLID principles, the complete domain model, refactoring anti-patterns, and testing strategy — putting it all together.

Prereq: OOD Module 7Python 3UML 2.5SysML v2
1

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.

diagram
Figure 2 — The five SOLID principles and how they apply to our bookshop domain.

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.

⚠️ Pitfall
A GodBook that holds data AND processes payments AND manages stock AND sends emails violates SRP. Changes to email format require touching book data logic — unrelated concerns coupled together.

O — Open/Closed Principle

Open for extension, closed for modification. Adding AudioBook only requires a new subclass — no existing code changes:

Python
# 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 automatically

L — Liskov Substitution Principle

Subtypes must be usable wherever the parent type is expected, without breaking the program:

Python
# 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 LSP

I — 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:

Python
# 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()
2

The Complete Domain Model

All eight modules integrated into one coherent model:

diagram
Figure 1 — Complete bookshop domain: inheritance hierarchies (Book, Customer), composition (Order◆OrderItem, Order◆Payment), aggregation (Cart◇Book, Inventory◇Book), interfaces (Payable, Discountable, Serialisable), Observer, and Strategy — all visible in one 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.

3

Refactoring Anti-Patterns

Anti-patternSymptomRefactoring
God ClassOne class with 50+ methods spanning multiple concerns.Extract to separate classes by responsibility (SRP).
Primitive ObsessionISBN as raw str, price as float with no validation scattered everywhere.Introduce value objects: ISBN, Money(amount, currency).
Shotgun SurgeryAdding one feature requires changing 10 different classes.Move related behaviour into one class (OCP, DIP).
Feature EnvyA method in Order constantly calls methods on Customer.Move the method to Customer (SRP).
Magic if/elif Chainsif book_type=="digital"... elif book_type=="physical"...Replace with polymorphism or Factory Method (OCP).
4

Testing OOD Code

Unit tests, contract tests (LSP enforcement), and integration tests with mock observers:

Python
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)
💡 CONTRACT TESTS = LSP ENFORCEMENT
The BookContractTests mixin is the direct mechanical payoff of LSP: if every Book implementation passes the same contract suite, they are all safely substitutable. Add the mixin to every new Book subclass test.
5

Series Retrospective

What you have built across the eight modules:

ModuleConceptApplied to
1Objects & ClassesBook, Customer, ShoppingCart
2EncapsulationPrivate attrs, @property, validation
3InheritanceDigitalBook, PhysicalBook, PremiumCustomer
4Polymorphismget_details(), dunder methods, duck typing
5Composition & AggregationOrder◆OrderItem, Cart◇Book
6Interfaces & ContractsPayable, Discountable, Protocol
7Design PatternsFactory, Singleton, Observer, Strategy
8SOLID & PracticeFull integration, testing, refactoring
ℹ️ NEXT STEPS
Natural continuations: concurrency and thread-safety for Inventory, persistence via the Repository pattern, REST API design over the domain, and advanced static analysis with mypy/Pyright.
6

Summary

SRP

Each class has one reason to change. Group related behaviour; separate unrelated concerns.

OCP

Extend by adding new classes, not modifying existing ones. Polymorphism and Strategy are the mechanisms.

LSP

Every subtype must honour the parent contract. Contract tests enforce this mechanically.

ISP

Prefer many small interfaces. Clients depend only on what they use.

DIP

Depend on abstractions. Inject through constructors. Enables testing with mock implementations.

Contract Tests

The BookContractTests mixin is the direct mechanical enforcement of LSP.

📚
Series Complete — What next?

Concurrency, the Repository pattern for persistence, REST API design over the domain, and advanced static analysis with mypy/Pyright.