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

第三模块:继承与层次结构

通过类层次结构复用和专态化行为——将 Book 扩展为 DigitalBook 和 PhysicalBook,并引入抽象类。

预备知识:OOD 第二模块 Python 3 ABC super() 里氏替换原则 SysML v2 对应
1

“Is-A” 关系

🏠 生活类比
想象书店的商品目录:每本书(无论是电子书还是纸质书)都有书名、作者、价格。但电子书有下载链接,纸质书有重量和仓库信息。继承就是表达"电子书是一种书"这个关系——共享通用属性,各自具备独特功能。

继承建模"is-a"关系。DigitalBook 是一种 BookPremiumCustomer 是一种 Customer。子类继承父类的全部属性和方法,并可在此基础上添加自己的内容,或修改继承来的方法。

关系类型使用时机
is-a(是一种)继承。DigitalBook is-a Book——共享所有 Book 行为并添加自己的运作。
has-a(拥有)组合。ShoppingCart has Books——它持有 Book 对象的引用,但本身不是 Book。
💡 黄金法则
只有当子类真正是父类的专态化版本时,才使用继承——它应当可以在任何需要父类的地方被使用(里氏替换原则)。如果你发现自己在重写方法时要么不做任何事要么抛出错误,那么组合几乎肯定是正确的选择。
2

单一继承

我们的书店销售两种书:数字下载和纸质印刷。两种都共享核心 Book 属性,但各有其独特的额外属性。继承让我们无需复制任何代码就能表达这种关系:

Python
from abc import ABC, abstractmethod

class Book(ABC):
    """所有书籍类型的抽象基类。"""

    def __init__(self, isbn, title, author, price, stock=0):
        if not isbn.strip(): raise ValueError("ISBN 不能为空")
        self.isbn = isbn; self.title = title; self.author = author
        self._price = 0.0; self._stock = 0
        self.price = price; self.stock = stock

    @abstractmethod
    def get_details(self) -> str:
        """每个子类必须提供自己的描述。"""
        ...

    def is_available(self): return self.stock > 0


class DigitalBook(Book):
    """可下载的电子书。"""
    def __init__(self, isbn, title, author, price,
                 file_format, file_size, download_url):
        super().__init__(isbn, title, author, price, stock=999)
        self.file_format  = file_format   # 如 "EPUB"、"PDF"
        self.file_size    = file_size     # 单位:MB
        self.download_url = download_url

    def get_details(self) -> str:
        return f"{self.title} 作者:{self.author} | {self.file_format} ({self.file_size:.1f} MB) £{self.price:.2f}"

    def download(self): print(f"下载自 {self.download_url}")


class PhysicalBook(Book):
    """存放在仓库中的实体书籍。"""
    SHIPPING_RATE = 0.005  # 英镑/克

    def __init__(self, isbn, title, author, price, stock,
                 weight, dimensions, warehouse):
        super().__init__(isbn, title, author, price, stock)
        self.weight = weight; self.dimensions = dimensions
        self.warehouse = warehouse

    def get_details(self) -> str:
        return f"{self.title} 作者:{self.author} | 纸质书 £{self.price:.2f} ({self.weight}克)"

    def get_shipping_cost(self) -> float:
        return round(self.weight * self.SHIPPING_RATE, 2)
UML 类层次:Book、DigitalBook、PhysicalBook
图 1 — UML 类层次:抽象类 Book 与两个具体子类。空心箭头表示泛化(继承)。«abstract» 标注基类和抽象方法。
3

构造函数链式调用 super()

🏠 生活类比
就像入职一家公司:在你开始自己的工作之前,你必须先完成公司的入职培训(父类初始化)。跳过入职培训就直接上岗,你会缺少必要的工具和权限——对象也是如此,没有 super().__init__() 就缺少父类的全部初始化。
Python
digital = DigitalBook(
    isbn="978-0-13-468599-1", title="务实程序员",
    author="Hunt & Thomas", price=34.99,
    file_format="EPUB", file_size=8.4,
    download_url="https://books.example.com/pp.epub"
)

print(digital.isbn)           # "978-0-13-468599-1"
print(digital.is_available()) # True(stock=999 由 DigitalBook.__init__ 设置)
print(digital.get_details())  # "务实程序员 作者:Hunt & Thomas | EPUB (8.4 MB) £34.99"
digital.download()             # "下载自 https://..."
digital.apply_discount(10)
print(digital.price)          # 31.491
💡 super() 与 MRO
super() 并不意味着"直接调用父类",而是调用方法解析顺序(MRO)中的下一个类。对于单继承,这永远是直接父类,但在多重继承时这个区别至关重要。
4

方法重写

DigitalBookPhysicalBook 都重写了 get_details()。方法名称相同,实现不同。Python 在运行时根据对象的实际类型自动选择正确版本:

Python
books = [
    DigitalBook("978-1-11", "清洁代码", "Martin", 29.99, "PDF", 5.2, "..."),
    PhysicalBook("978-1-22", "重构", "Fowler", 39.99, 5, 520, "...", "WH1"),
]

for book in books:
    print(book.get_details())   # 自动调用正确版本

# 清洁代码 作者:Martin | PDF (5.2 MB) £29.99
# 重构 作者:Fowler | 纸质书 £39.99 (520克)

这就是多态——同一个方法调用根据对象的类型产生不同的行为。第四模块将深入讲解多态。

⚠️ 注意——违反里氏替换原则
重写不能改变方法的合同——不能要求不同的参数、返回不同的类型,或抛出父类不会抛出的异常。操作任何 Book 的代码必须同样适用于 DigitalBookPhysicalBook
5

抽象类与 @abstractmethod

抽象类无法直接实例化——它只存在于被继承。@abstractmethod 强制每个具体子类提供自己的实现:

Python
# 无法实例化抽象类:
book = Book("978-0", "测试", "作者", 9.99)
# TypeError: 无法实例化抽象类 Book
#            它有未实现的抽象方法 get_details

# 忘记实现 get_details 的子类同样失败:
class BrokenBook(Book): pass

broken = BrokenBook("978-0", "测试", "作者", 9.99)
# TypeError: 无法实例化抽象类 BrokenBook
💡 设计意图
@abstractmethod 是一个合同:"每个具体 Book 都必须知道如何描述自己。"抽象类定义接口,每个子类提供实现。这是多态的基础。

PremiumCustomer 子类

Python
class PremiumCustomer(Customer):
    """参与积分计划的顾客。"""

    def __init__(self, customer_id, name, email, discount_rate=0.10):
        super().__init__(customer_id, name, email)
        if not 0 <= discount_rate <= 1:
            raise ValueError("折扣率必须在0到1之间")
        self._loyalty_points = 0
        self._discount_rate  = discount_rate

    def add_points(self, points: int):
        if points <= 0: raise ValueError("积分必须为正整数")
        self._loyalty_points += points

    def get_details(self) -> str:
        return (f"{self.get_name()} (高级会员) | "
                f"积分:{self._loyalty_points} | "
                f"折扣:{self._discount_rate*100:.0f}%")
UML:Customer 与 PremiumCustomer
图 2CustomerPremiumCustomer。空心箭头表示继承。PremiumCustomer 继承所有 Customer 属性和方法,并添加积分计划行为。
6

组合 vs 继承

继承功能强大但常被滥用。一个有用的经验法则:除非明确满足 is-a 测试且不太可能改变,否则首选组合而非继承。

因素倾向继承倾向组合
关系"Is-a":DigitalBook 是一种 Book"Has-a":ShoppingCart 拥有 Books
灵活性层次稳定,不大可能改变行为可能在运行时变化或重配置
耦合度紧耦合可接受——子类了解父类较松耦合——组件相互独立
💡 实用原则
如果层次结构超过2–3层,或者需要从多个无关类继承,请停下来考虑组合是否更能清楚地表达关系。深层次结构非常脆弱——对基类的一个改动会波及所有子类。
7

SysML v2 对应

SysML v2 为继承提供了直接的语言构造,映射非常精确:

Python / OODSysML v2
class Child(Parent)part def Child :> Parent { ... }
super().__init__()隐式——继承特征自动可用
重写方法:>> methodName { ... }
抽象类(ABC)abstract part def Book { ... }
@abstractmethodabstract action def get_details : String;
Python / OOD
SysML v2
class Child(Parent)
part def Child :> Parent
重写方法
:>> methodName
抽象类(ABC)
abstract part def
@abstractmethod
abstract action def
💡 :> 与 :>> 的区别
:>(子类化)声明"此 part def 是那个的专态化"——等价于类继承。:>>(重定义)重写父类中的某个具体特征——等价于方法重写。两者配合使用::> 建立层次,:>> 在层次内定制具体特征。
8

总结

Is-A

只有当子类真正是父类的专态化且满足里氏替换原则时,才使用继承。

super()

子类构造函数中必须调用 super().__init__(),确保父类在添加子类状态之前被完整初始化。

方法重写

每个子类提供自己的实现。Python 在运行时自动选择正确版本。合同必须被保留。

抽象类

ABC + @abstractmethod 定义所有具体子类必须实现的接口。无法直接实例化。

组合

当 is-a 关系不明确、层次可能很深或需要运行时灵活性时,首选组合而非继承。

SysML v2

:> 声明子类化,:>> 重定义特征。abstract part defabstract action def 强制执行抽象类模式。

🧬
下一讲 — 第四模块

多态

同一个方法调用如何根据对象的类型产生不同的行为、Python 中的鸭子类型、运算符重载,以及 UML 和 SysML v2 中的多态全景。