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

第二模块:封装

隐藏数据、设计清晰的公开接口,并引入书店领域的第三个类——ShoppingCart。

预备知识:OOD 第一模块 Python 3 @property 迪米特法则 SysML v2 对应
1

封装要解决的问题

🏠 生活类比
想象一个公共图书馆,所有书籍全部预放在柜台上且无任何工作人员看管。任何人都可以随手添写、涂改书籍中的任何内容。封装就是设置这个看管员的过程:只有当你请求服务时,内容才会按规则改变。

在第一模块中,Book 类的所有属性均为公开属性。程序中任何地方的代码都可以直接读写它们:

Python
book1.price = -9.99    # 非法:价格不能为负
book1.stock = "lots"   # 非法:库存必须是整数
book1.isbn  = ""       # 非法:ISBN 不能为空
# 对象现在处于错误状态。没有任何报错。
关切点含义
公开接口外部允许调用的操作。稳定、有意为之、有文档记录。
私有实现内部数据与辅助逻辑。隐藏、可更改而不破坏调用方。
💡 核心原则
面向接口编程,而非实现。外部代码只应依赖类承诺做什么,而非内部如何存储数据。
2

Python 中的访问修饰符

🏠 生活类比
就像酒店房间:门上没有锁(Python 不强制),但"请勿打扰"牌子(_ 前缀)表示你不应该进去;而贴有"仅限员工"(__ 前缀)的门则强烈暗示这是内部区域。
命名惯例可见性含义
name公开类公开接口的一部分,可在任意地方安全使用。
_name受保护内部使用,供类本身及其子类使用。不鼓励外部访问。
__name私有触发名称改写。强烈表示:内部实现细节,请勿触动。
Python
class Book:
    def __init__(self, isbn, title, author, price, stock=0):
        self.isbn    = isbn    # 公开  — 外部可直接读取
        self.title   = title
        self.author  = author
        self._price  = price   # 受保护 — 建议通过 property 访问
        self._stock  = stock   # 受保护 — 建议通过 property 访问
3

属性访问器 — @property

🏠 生活类比
想象一个现金出纳窗口:你不能直接伸手进去往金库拿钱。你必须告诉首席你想存款取款多少,首席按规则处理。@property 就是那个首席——从外部看仍然是直接访问属性,但内部有验证逻辑在保护。
Python
    @property
    def price(self) -> float:
        return self._price

    @price.setter
    def price(self, value: float) -> None:
        if value < 0:
            raise ValueError(f"价格不能为负: {value}")
        self._price = value

    @property
    def stock(self) -> int:
        return self._stock

    @stock.setter
    def stock(self, value: int) -> None:
        if not isinstance(value, int) or value < 0:
            raise ValueError("库存必须是非负整数")
        self._stock = value
Python — 调用方视角
book1.price = 39.99      # 正常,通过 setter
book1.price = -5.00      # ValueError: 价格不能为负: -5.0
book1.stock = "lots"     # ValueError: 库存必须是非负整数
💡 设计原则
只有当需要验证、计算属性值或未来需要更改内部存储方式时,才需要将属性封装为 property。无需验证的简单数据可以保持为普通公开属性。
4

类不变量

类不变量是对类的每个有效实例必须始终成立的条件。对于 Book 类:

  • price ≥ 0 — 书籍不能有负价格。
  • stock ≥ 0 且为整数。
  • isbn 非空 — 每本书必须有唯一标识符。
Python — 构造函数守卫
    def __init__(self, isbn: str, ...):
        if not isbn or not isbn.strip():
            raise ValueError("ISBN 不能为空")
        self.isbn = isbn
⚠️ 注意
不变量必须在每个可能修改相关状态的方法中执行检查——而不仅仅是构造函数。每个可变状态的入口点都是潜在的不变量违反点。
5

引入 ShoppingCart

领域的第三个类。它持有一组 (Book, 数量) 配对,展示了封装在一个包含可变集合的复杂类上的应用:

Python
class ShoppingCart:
    """属于一个顾客的购物篮。"""

    def __init__(self, customer):
        self._customer = customer        # 私有:拥有者
        self._items: list[tuple] = []    # 私有:(Book, int) 列表

    def add_item(self, book, quantity: int = 1) -> None:
        if quantity <= 0:
            raise ValueError("数量必须为正整数")
        if not book.is_available():
            raise ValueError(f"{book.title} 已缺货")
        for i, (b, q) in enumerate(self._items):
            if b.isbn == book.isbn:
                self._items[i] = (b, q + quantity)
                return
        self._items.append((book, quantity))

    def get_total(self) -> float:
        return sum(b.price * q for b, q in self._items)

    @property
    def items(self) -> list:
        return list(self._items)   # 返回副本而非引用
💡 关键细节
items 返回 list(self._items)——内部列表的副本,而非其引用。如果直接返回 self._items,调用方可以在类外变更内部列表,绕过所有验证逻辑。
UML 类图:Book 与 ShoppingCart
图 1 — UML 类图:Book(带 property)与 ShoppingCart。«property» 构造型标注受控属性访问。
6

设计清晰的公开接口

迪米特法则

一个方法只应调用以下对象的方法:对象自身、传入的参数、方法内部创建的对象,以及 self 的直接属性。只用一个点——不要穿越一个对象去调用另一个对象的方法。

Python
# 差 — 违反迪米特法则(两跳:cart → book → str)
for book, qty in cart.items:
    print(book.author.upper())

# 好 — 让 book 自己暴露所需信息
for book, qty in cart.items:
    print(book.get_details())

封装的 Customer 类

Python
class Customer:
    def __init__(self, customer_id, name, email):
        self._customer_id = customer_id
        self._name        = name
        self._email       = ""
        self.email        = email   # 调用 setter 进行验证

    @property
    def email(self): return self._email

    @email.setter
    def email(self, value):
        if "@" not in value:
            raise ValueError(f"无效的电子邮件地址: {value}")
        self._email = value

    def update_email(self, new_email):
        self.email = new_email   # 通过 setter
关联图:Customer 拥有 ShoppingCart
图 2 — 关联图:Customer 拥有一个 ShoppingCart。标签"owns"与多重度(1 — 1..1)是标准 UML 关联记法。
7

SysML v2 对应

SysML v2 直接支持在特征(feature)上使用 UML 可见性标记。从 Python 惯例到 SysML v2 的映射非常直接:

Python 惯例SysML v2 对应
name — 公开属性public attribute name : Type;
_name — 受保护属性protected attribute _name : Type;
__name — 私有属性private attribute __name : Type;
+ 公开方法public action def method;
私有方法private action def method;
Python
SysML v2
公开 name
public attribute
受保护 _name
protected attribute
私有 __name
private attribute
@property
derived attribute
💡 命名空间作用域
在 SysML v2 中,private 可见性将访问限制在拥有命名空间内(part def 块内)。protected 可见性允许专一化(子类)访问。这与 Python 的 ___ 惯例直接映射。
8

总结

封装

将公开接口与私有实现分离。外部代码只依赖接口,而非内部数据布局。

访问惯例

name 公开,_name 受保护,__name 私有。惯例为基础,所有严谨的 Python 代码均遵守。

@property

为看似普通的属性赋值操作添加验证逻辑。需要执行不变量时使用。

类不变量

每个有效实例任何时刻必须成立的条件。封装保证只有类自身的方法能改变其状态。

返回副本

通过 property 暴露可变内部状态时,应返回副本而非引用,防止外部代码绕过验证直接修改私有数据。

迪米特法则

只调用 self、参数、局部创建对象和直接属性的方法。用一个点,不用链式调用。

🧬
下一讲 — 第三模块

继承与层次结构

一个类如何继承另一个类,如何复用和专态化行为,以及如何在 UML 和 SysML v2 中建模泛化层次结构。