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

第六模块:接口与契约

定义类必须履行的正式契约——抽象基类、Protocol、契约式设计与SysML v2 interface def。

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

为什么需要接口?

🏠 生活类比
想象一个电源插座接口:它规定了插头的形状和电压标准(规范),但不关心连接的是台灯、手机充电器还是电脑(实现)。只要符合规范,任何设备都能使用。

接口定义了类能做什么,而不指定怎么做。依赖接口的代码与任何特定实现解耦:

机制子类型模型工作方式
ABC名义子类型类必须显式继承ABC并实现所有抽象方法。缺少任何方法时Python在实例化时抛出TypeError。
Protocol结构子类型任何拥有所需方法的类都兼容,无论继承关系如何。无需显式声明。
ℹ️ 接口强制执行
使用ABC:在受控代码库内部进行严格强制。使用Protocol:对外部类型和测试替身进行灵活结构化类型检查。
2

抽象基类作为接口

为书店领域定义三个精简接口,每个捕获单一职责:

Python
from abc import ABC, abstractmethod
from typing import TypeVar, Type
T = TypeVar("T")

class Payable(ABC):
    """可以支付和退款的任何事物。"""
    @abstractmethod
    def process(self) -> bool: ...
    @abstractmethod
    def refund(self) -> bool: ...

class Discountable(ABC):
    """可以应用折扣的任何事物。"""
    @abstractmethod
    def apply_discount(self, percentage: float) -> None: ...
    @abstractmethod
    def get_discount_rate(self) -> float: ...

class Serialisable(ABC):
    """可以序列化为/从字典的任何事物。"""
    @abstractmethod
    def to_dict(self) -> dict: ...
    @classmethod
    @abstractmethod
    def from_dict(cls: Type[T], data: dict) -> T: ...

一个类可以同时实现多个ABC(Python多重继承):

Python
# Book同时实现Discountable和Serialisable
class Book(ABC, Discountable, Serialisable):
    def apply_discount(self, percentage: float) -> None:
        if not 0 <= percentage <= 100:
            raise ValueError(f"折扣必须在0-100之间,得到{percentage}")
        self._price = round(self._price * (1 - percentage/100), 2)

    def get_discount_rate(self) -> float: return self._discount

    def to_dict(self) -> dict:
        return {"isbn":self.isbn,"title":self.title,"price":self._price}

# Payment实现Payable
class Payment(Payable):
    def process(self) -> bool: self._status="complete"; return True
    def refund(self) -> bool:
        if self._status!="complete": raise RuntimeError("未完成付款,无法退款")
        self._status="refunded"; return True
diagram
图1 — Payable由Payment实现。Discountable和Serialisable都由Book实现。Python允许一个类同时实现多个ABC。
ℹ️ 抽象方法强制执行
如果子类没有实现所有抽象方法,Python在实例化时(而非方法调用时)会引发TypeError。您无法创建不完整实现的实例。
3

Protocol:结构子类型

任何拥有正确方法的类都满足Protocol——不需要继承。非常适合第三方类型和测试替身:

Python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Priceable(Protocol):
    """拥有可折扣价格的任何事物。"""
    @property
    def price(self) -> float: ...
    def apply_discount(self, percentage: float) -> None: ...

# GiftCard满足Priceable,无需继承它
class GiftCard:
    def __init__(self, code, value):
        self.code=code; self._price=value
    @property
    def price(self): return self._price
    def apply_discount(self, pct):
        self._price = round(self._price * (1 - pct/100), 2)

def apply_sale_price(item: Priceable, pct: float) -> None:
    item.apply_discount(pct); print(f"  新价格:£{item.price:.2f}")

book = DigitalBook("978","清洁代码","Martin",29.99,"PDF",5.2,"...")
card = GiftCard("GC-001", 25.00)
apply_sale_price(book, 10)   # 可用——Book有price和apply_discount
apply_sale_price(card, 10)   # 可用——GiftCard结构上满足Priceable
print(isinstance(card, Priceable))  # True(因为@runtime_checkable)
diagram
图2 — Book(继承Discountable)和GiftCard(无继承)都满足Priceable协议,因为它们拥有所需方法。不需要继承关系。
💡 何时使用哪种机制
使用ABC:需要通过继承进行严格强制,类不完整时能立即得到TypeError。使用Protocol:需要灵活的结构类型——尤其适用于测试模拟对象和无法修改的第三方类。
4

契约式设计

契约式设计将前置条件(方法期望什么)、后置条件(方法保证什么)和不变量(始终成立的条件)形式化:

Python
class Book(ABC, Discountable, Serialisable):
    """
    类不变量:isbn为非空字符串,_price >= 0,0 <= _discount <= 100
    """
    def apply_discount(self, percentage: float) -> None:
        """
        前置条件:0 <= percentage <= 100
        后置条件:self.price == old_price * (1 - percentage/100)
        """
        if not 0 <= percentage <= 100:
            raise ValueError(f"折扣必须在0-100之间,得到{percentage}")
        old_price = self._price
        self._discount = percentage
        self._price = round(old_price * (1 - percentage/100), 2)
        assert self._price >= 0, "价格不变量被违反"  # 后置条件
ℹ️ 契约层次
前置条件:在方法入口用if/raise检查。后置条件:在方法出口用assert检查(生产环境可用-O标志禁用)。不变量:在属性getter/setter中检查。
5

SysML v2 对应

SysML v2
interface def Payable {
    action def process : Boolean;
    action def refund  : Boolean;
}
interface def Discountable {
    action def apply_discount { in percentage : Real; }
}
// Book同时实现多个接口
abstract part def Book :> Discountable, Serialisable {
    attribute isbn  : String; attribute price : Real;
    abstract action def get_details : String;
}
Python / OOD
SysML v2
ABC + @abstractmethod
interface def + abstract actions
class X(InterfaceA, InterfaceB)
part def X :> InterfaceA, InterfaceB
Protocol(结构化)
通过方法签名的结构一致性
前置条件(if/raise)
action def中的require约束
后置条件(assert)
action def中的ensure约束
6

总结

接口

将规范(做什么)与实现(怎么做)分离。代码依赖接口,而非具体类。

ABC(名义)

需要显式继承。Python在实例化时强制完整性。适合受控代码库。

Protocol(结构)

拥有正确方法即可满足,无需继承。适合外部类型和测试替身。

多重接口

class X(ABC_A, ABC_B)支持ISP——小而专注的契约优于一个大接口。

契约式设计

前置条件(if/raise)、后置条件(assert)、不变量使契约显式可检查。

SysML v2

interface def + part def X :> Interface直接映射Python ABC。

📚
下一讲 — 第七模块:设计模式

GoF经典模式应用于书店:工厂方法、单例、观察者、策略与装饰器,每种模式附SysML v2映射。