并发、Repository持久化模式、REST API设计,以及使用mypy/Pyright的高级静态分析。
第八模块:OOD实践
SOLID原则、完整领域模型、重构反模式与测试策略——融会贯通。
SOLID原则
SOLID是五个原则的助记符,综合应用后能产生易于理解、扩展和测试的代码:
S — 单一职责原则
一个类应该只有一个改变的理由。Book处理数据和验证,Order处理生命周期,Payment处理交易。每个类只因自己的原因而改变。
O — 开放/封闭原则
对扩展开放,对修改封闭。添加AudioBook只需新建子类——现有代码无需修改:
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} 作者:{self.author} | 有声书({self.duration_hrs}小时)£{self.price:.2f}"
# 无需修改:Inventory、ShoppingCart、BookFactory(只需添加"audio"分支)L — 里氏替换原则
子类型必须在父类型期望的任何地方都能正确工作:
def apply_sale(books: list[Book], pct: float) -> None:
for b in books:
b.apply_discount(pct) # 契约:按pct降价
assert b.price >= 0 # 不变量:价格非负
assert b.get_discount_rate()==pct # 后置条件
# DigitalBook 通过:履行所有Book契约
# PhysicalBook 通过:履行所有Book契约
# 忽略apply_discount()的BadBook 违反LSPI — 接口隔离原则
优先使用多个小接口而非一个大接口。我们有Payable(2个方法)、Discountable(2个方法)、Serialisable(2个方法),而非一个含20个方法的IBook接口。支付处理器只依赖Payable。
D — 依赖倒置原则
依赖抽象,而非具体实现。通过构造函数注入依赖:
class Order:
def __init__(self, order_id, customer,
pricing: PricingStrategy | None = None,
inventory: Inventory | None = None):
self._pricing = pricing or StandardPricing()
self._inventory = inventory or Inventory.get_instance()
# Order依赖PricingStrategy(抽象),而非StandardPricing(具体)
# 测试中:注入MockPricing()和InMemoryInventory()完整领域模型
八个模块集成为一个连贯的模型:
从上到下阅读模型:Book是抽象核心。DigitalBook和PhysicalBook专态化它。Customer和PremiumCustomer构成第二个层次。ShoppingCart聚合Books。Order组合OrderItem和Payment(后者实现Payable)。Inventory聚合Books并通知StockObserver。Order使用PricingStrategy。
重构反模式
| 反模式 | 症状 | 重构方法 |
|---|---|---|
| 上帝类 | 一个类有50+方法,跨越多个关注点。 | 按职责提取为独立类(SRP)。 |
| 基本类型偏执 | ISBN作为原始str散布各处;价格作为无验证的float。 | 引入值对象:ISBN(str)、Money(float, currency)。 |
| 霰弹枪手术 | 添加一个功能需要修改10个不同的类。 | 将相关行为移入一个类(OCP、DIP)。 |
| 特性依恋 | Order中的方法不断调用Customer的方法。 | 将方法移至Customer(SRP)。 |
| 魔法if/elif链 | if book_type=="digital"... elif =="physical"... | 用多态或工厂方法替换(OCP)。 |
测试OOD代码
单元测试、契约测试(LSP强制)和带模拟观察者的集成测试:
import pytest; from unittest.mock import MagicMock
# 单元测试:隔离测试单个类
class TestBook:
def setup_method(self):
self.book=DigitalBook("978","清洁代码","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)
def test_discount_out_of_range(self):
with pytest.raises(ValueError): self.book.apply_discount(150)
# 契约测试:任何Book实现都必须通过
class BookContractTests:
book: Book # 子类的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 TestDigitalContracts(BookContractTests):
def setup_method(self): self.book=DigitalBook("978","T","A",30.0,"EPUB",3.0,"url")
# 集成测试(带模拟观察者)
class TestInventoryObserver:
def test_notifies_on_stock_change(self):
inv=Inventory(); inv._observers.clear()
observer=MagicMock(); inv.subscribe(observer)
book=DigitalBook("978","清洁代码","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)系列回顾
八个模块中你构建了什么:
| 模块 | 概念 | 应用对象 |
|---|---|---|
| 1 | 对象与类 | Book, Customer, ShoppingCart |
| 2 | 封装 | 私有属性、@property、验证 |
| 3 | 继承 | DigitalBook, PhysicalBook, PremiumCustomer |
| 4 | 多态 | get_details()、魔法方法、鸭子类型 |
| 5 | 组合与聚合 | Order◆OrderItem, Cart◇Book |
| 6 | 接口与契约 | Payable, Discountable, Protocol |
| 7 | 设计模式 | 工厂、单例、观察者、策略 |
| 8 | SOLID与实践 | 完整集成、测试、重构 |
总结
每个类只有一个改变的理由。将相关行为分组;分离不相关的关注点。
通过添加新类来扩展,而非修改现有类。多态和策略是主要机制。
每个子类型必须履行父类契约。契约测试在机械层面强制执行这一点。
优先使用多个小接口。客户端只依赖它们使用的方法。
依赖抽象。通过构造函数注入。支持用模拟实现进行测试。
BookContractTests混入是LSP的直接机械化强制执行。