Ran Wei /面向对象设计/第八模块
EN
面向对象设计 — Ran Wei

第八模块:OOD实践

SOLID原则、完整领域模型、重构反模式与测试策略——融会贯通。

预备知识: OOD 第七模块Python 3UML 2.5SysML v2
1

SOLID原则

🏠 生活类比
想象建筑一栋大楼:结构工程师、电气工程师和管道工各司其职(SRP)。新功能通过添加模块实现,不拆改现有结构(OCP)。标准接口确保任何符合标准的设备都能接入(LSP、ISP、DIP)。

SOLID是五个原则的助记符,综合应用后能产生易于理解、扩展和测试的代码:

diagram
图2 — 五大SOLID原则及其在书店领域中的应用。

S — 单一职责原则

一个类应该只有一个改变的理由。Book处理数据和验证,Order处理生命周期,Payment处理交易。每个类只因自己的原因而改变。

⚠️ 注意
违反示例:一个GodBook类既保存数据又处理支付又管理库存又发送邮件。修改邮件格式需要触碰图书数据逻辑——不相关的关注点被耦合在一起。

O — 开放/封闭原则

对扩展开放,对修改封闭。添加AudioBook只需新建子类——现有代码无需修改:

Python
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 — 里氏替换原则

子类型必须在父类型期望的任何地方都能正确工作:

Python
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 违反LSP

I — 接口隔离原则

优先使用多个小接口而非一个大接口。我们有Payable(2个方法)、Discountable(2个方法)、Serialisable(2个方法),而非一个含20个方法的IBook接口。支付处理器只依赖Payable。

D — 依赖倒置原则

依赖抽象,而非具体实现。通过构造函数注入依赖:

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

完整领域模型

八个模块集成为一个连贯的模型:

diagram
图1 — 完整书店领域模型:继承层次(Book、Customer)、组合(Order◆OrderItem、Order◆Payment)、聚合(Cart◇Book、Inventory◇Book)、接口(Payable、Discountable、Serialisable)、观察者和策略模式,所有内容在一个模型中可见。

从上到下阅读模型:Book是抽象核心。DigitalBook和PhysicalBook专态化它。Customer和PremiumCustomer构成第二个层次。ShoppingCart聚合Books。Order组合OrderItem和Payment(后者实现Payable)。Inventory聚合Books并通知StockObserver。Order使用PricingStrategy。

3

重构反模式

反模式症状重构方法
上帝类一个类有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)。
4

测试OOD代码

单元测试、契约测试(LSP强制)和带模拟观察者的集成测试:

Python
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)
💡 契约测试 = LSP强制
BookContractTests混入是LSP的直接机械化强制:如果每种Book实现都通过同一契约测试套件,它们就是安全可替换的。为每个新Book子类的测试添加此混入。
5

系列回顾

八个模块中你构建了什么:

模块概念应用对象
1对象与类Book, Customer, ShoppingCart
2封装私有属性、@property、验证
3继承DigitalBook, PhysicalBook, PremiumCustomer
4多态get_details()、魔法方法、鸭子类型
5组合与聚合Order◆OrderItem, Cart◇Book
6接口与契约Payable, Discountable, Protocol
7设计模式工厂、单例、观察者、策略
8SOLID与实践完整集成、测试、重构
ℹ️ 后续学习方向
自然的后续主题:Inventory的并发与线程安全、通过Repository模式实现持久化、在领域上层设计REST API,以及使用mypy/Pyright进行高级静态分析。
6

总结

SRP

每个类只有一个改变的理由。将相关行为分组;分离不相关的关注点。

OCP

通过添加新类来扩展,而非修改现有类。多态和策略是主要机制。

LSP

每个子类型必须履行父类契约。契约测试在机械层面强制执行这一点。

ISP

优先使用多个小接口。客户端只依赖它们使用的方法。

DIP

依赖抽象。通过构造函数注入。支持用模拟实现进行测试。

契约测试

BookContractTests混入是LSP的直接机械化强制执行。

📚
系列完结 — 后续学习

并发、Repository持久化模式、REST API设计,以及使用mypy/Pyright的高级静态分析。