设计原则构成任何良好架构软件的基石。它们像指路明灯,帮助开发者在避免糟糕设计陷阱的同时,走上构建可维护、可扩展且健壮应用的正确道路。
本章将探讨所有开发者在项目中都应了解并践行的核心设计原则。我们会讨论四条基础原则。第一条“封装变化(Encapsulate What Varies)”教你如何隔离代码中可能发生变化的部分,从而更容易修改与扩展应用。接着,“多用组合(Favor Composition)”让你理解为何通常应当用简单对象的组装来构建复杂对象,而不是通过继承来获取功能。第三条“面向接口编程(Program to Interfaces)”展示了针对接口而非具体类进行编码的力量,提升灵活性与可维护性。最后,通过“松散耦合(Loose Coupling)”原则,你将把握降低组件间依赖的重要性,使代码更易于重构与测试。
本章将涵盖以下主要主题:
- 遵循“封装变化(Encapsulate What Varies)”原则
- 遵循“多用组合、少用继承(Favor Composition Over Inheritance)”原则
- 遵循“面向接口而非实现(Program to Interfaces, Not Implementations)”原则
- 遵循“松散耦合(Loose Coupling)”原则
在本章结束时,你将扎实掌握这些原则,并学会如何在 Python 中实践它们,为全书其余内容打下基础。
技术要求
阅读本书各章,你需要可运行的 Python 3.12 环境;在个别章节的少量特殊场景中,Python 3.11 也可使用。
此外,请安装 Mypy 静态类型检查器(www.mypy-lang.org),运行以下命令即可:,:-mp7i01dha677i8jbvycg046au8va/)
python3.12 -m pip install --user mypy
示例代码位于 GitHub 仓库:
github.com/PacktPublis…
关于 Python 可执行文件
全书中我们将以
python3.12或python来指代用于执行示例代码的 Python 可执行文件。请根据你自己的环境、惯例和工作流进行相应调整与替换。
遵循“封装变化(Encapsulate What Varies)”原则
软件开发中最常见的挑战之一是应对变化。需求会演进,技术在进步,用户需求也会改变。因此,至关重要的是编写能够适应变化、且不会在整个程序或应用中引发连锁修改的代码。这正是“封装变化”原则发挥作用的地方。
这意味着什么?
该原则背后的思想很直接:将最可能发生变化的代码部分隔离并加以封装。这样,你就为这些易变元素建立了一道“防护墙”,从而保护其余代码不受影响。通过这种封装,你可以在系统的一处进行修改,而不必波及其他部分。
好处
封装变化带来多方面收益,主要包括:
- 易于维护:需要变更时,只需修改被封装的部分,降低在其他位置引入缺陷的风险。
- 灵活性增强:被封装的组件可以轻松替换或扩展,使架构更具适应性。
- 可读性提升:将可变要素隔离后,代码更有条理,也更易理解。
实现封装的技术
正如我们介绍的,封装有助于隐藏数据,仅暴露必要的功能。这里将介绍在 Python 中增强封装的关键技术:多态(polymorphism)以及Getter/Setter 技术。
多态(Polymorphism)
在编程中,多态允许不同类的对象被当作同一父类的对象来对待。它是面向对象编程(OOP)的核心概念之一,使得一个统一的接口可以表示不同的类型。多态有助于实现诸如策略模式等优雅的设计模式,也是编写整洁、可维护 Python 代码的有效途径。
Getter 与 Setter
这是类中的特殊方法,用于对属性值进行受控访问:getter 用于读取属性值,setter 用于修改属性值。通过这些方法,你可以加入校验逻辑或副作用(例如日志记录),从而遵循封装原则。它们提供了一种控制并保护对象状态的方式,尤其适用于封装由其他实例变量派生出的复杂属性。
此外,为补充 Getter/Setter 技术,Python 还提供了一种更优雅的方式——property(属性)技术。这是 Python 的内置特性,允许将属性访问无缝转换为方法调用。借助属性,你可以确保对象的内部状态免受不正确或有害的操作影响,而无需显式定义 getter 和 setter 方法。
@property 装饰器允许你定义一个在访问属性时自动调用的方法,相当于 getter。同样,@属性名.setter 装饰器允许你定义一个在尝试修改属性值时被调用的方法,相当于 setter。通过这种方式,你可以将校验或其他动作直接嵌入这些方法中,使代码更整洁。
使用 property 技术,你可以像传统 Getter/Setter 一样实现数据封装与校验,但更符合 Python 的设计哲学。它让你的代码不仅可用,而且简洁易读,同时提升封装程度与整体代码质量。
接下来,我们通过示例更好地理解这些技术。
示例——使用多态进行封装
多态是实现可变行为封装的有力方式。以下以支付处理系统为例,假设支付方式是可变的,那么你可以将每种支付方式封装到各自的类中:
首先定义支付方式的基类,提供一个 process_payment() 方法,每个具体的支付方式将实现该方法。我们在此处封装“会变化的部分”——支付处理逻辑。代码如下:
class PaymentBase:
def __init__(self, amount: int):
self.amount: int = amount
def process_payment(self):
pass
接着,引入 CreditCard 与 PayPal 两个类,继承自 PaymentBase,并各自实现 process_payment。这是经典的多态用法,因为你可以把 CreditCard 与 PayPal 对象当作它们的共同父类实例来处理。代码如下:
class CreditCard(PaymentBase):
def process_payment(self):
msg = f"Credit card payment: {self.amount}"
print(msg)
class PayPal(PaymentBase):
def process_payment(self):
msg = f"PayPal payment: {self.amount}"
print(msg)
为了测试上述类,加入一些代码,对每个对象调用 process_payment()。使用这些类时,多态的魅力便一目了然:
if __name__ == "__main__":
payments = [CreditCard(100), PayPal(200)]
for payment in payments:
payment.process_payment()
完整代码(ch01/encapsulate.py)如下:
class PaymentBase:
def __init__(self, amount: int):
self.amount: int = amount
def process_payment(self):
pass
class CreditCard(PaymentBase):
def process_payment(self):
msg = f"Credit card payment: {self.amount}"
print(msg)
class PayPal(PaymentBase):
def process_payment(self):
msg = f"PayPal payment: {self.amount}"
print(msg)
if __name__ == "__main__":
payments = [CreditCard(100), PayPal(200)]
for payment in payments:
payment.process_payment()
测试命令:
python3.12 ch01/encapsulate.py
你应能看到如下输出:
Credit card payment: 100
PayPal payment: 200
如你所见,当支付方式变化时,程序依然能够产出预期结果。通过封装“会变化的部分”——这里是支付方式——你可以轻松添加新选项或修改现有选项,而不影响核心的支付处理逻辑。
示例——使用 property(属性)进行封装
下面定义一个 Circle 类,演示如何使用 Python 的 @property 技术为其 radius(半径)属性创建 getter 与 setter。注意,底层实际属性名为 _radius,它被名为 radius 的 property 所“隐藏/保护”。
我们按步骤编写代码:
首先定义 Circle 类及其初始化方法,在其中初始化 _radius 属性:
class Circle:
def __init__(self, radius: int):
self._radius: int = radius
添加 radius 属性:定义一个 radius() 方法返回底层属性值,并使用 @property 进行装饰,代码如下:
@property
def radius(self):
return self._radius
添加 radius 的 setter:再定义一个同名 radius() 方法,先进行校验(例如不允许负值),通过 @radius.setter 进行装饰,代码如下:
@radius.setter
def radius(self, value: int):
if value < 0:
raise ValueError("Radius cannot be negative!")
self._radius = value
最后,添加几行代码便于测试该类:
if __name__ == "__main__":
circle = Circle(10)
print(f"Initial radius: {circle.radius}")
circle.radius = 15
print(f"New radius: {circle.radius}")
完整代码(ch01/encapsulate_bis.py)如下:
class Circle:
def __init__(self, radius: int):
self._radius: int = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value: int):
if value < 0:
raise ValueError("Radius cannot be negative!")
self._radius = value
if __name__ == "__main__":
circle = Circle(10)
print(f"Initial radius: {circle.radius}")
circle.radius = 15
print(f"New radius: {circle.radius}")
测试命令:
python3.12 ch01/encapsulate_bis.py
你应能看到如下输出:
Initial radius: 10
New radius: 15
在第二个示例中,我们看到如何封装圆的半径这一组件,从而在需要时改变技术细节而不破坏类的对外行为。例如,setter 的校验逻辑可以演进;甚至即便更改底层属性 _radius,对使用者而言的行为也将保持不变。
遵循“多用组合、少用继承(Favor Composition Over Inheritance)”原则
在面向对象编程(OOP)中,人们很容易通过继承构建复杂的类层次。继承固然有其优点,但也可能导致代码高度耦合、难以维护和扩展。因此,“多用组合、少用继承”原则应运而生。
这意味着什么?
该原则建议:与其从基类继承功能,不如用更简单的部件组合来构建对象。换言之,用多个简单对象的组合来搭建复杂对象。
好处
选择组合而非继承具有多项优势:
- 灵活性:组合允许在运行时改变对象行为,使代码更具适应性。
- 可复用性:更小、更简单的对象可以在应用的不同部分复用,促进代码复用。
- 易维护:采用组合时,可以轻松替换或更新单个组件,而不影响整体系统,避免连带影响。
组合的技术
在 Python 中,组合通常通过在一个类中包含其他类的实例来实现。这有时被称为“has-a(拥有)关系”,即被组合的类与所包含的类之间的关系。Python 的动态特性使组合更加容易——不需要显式类型声明。你可以在类的 __init__ 方法中直接实例化其他对象,或通过参数传入它们。
示例——用引擎组合出汽车
在 Python 中,你可以通过在类中包含其他类的实例来使用组合。例如,考虑一个 Car 类,其中包含一个 Engine 类的实例。
先定义 Engine 类及其 start 方法:
class Engine:
def start(self):
print("Engine started")
然后按如下方式定义 Car 类:
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()
print("Car started")
最后,添加以下代码,在程序执行时创建 Car 的实例并调用其 start 方法:
if __name__ == "__main__":
my_car = Car()
my_car.start()
完整代码(ch01/composition.py)如下:
class Engine:
def start(self):
print("Engine started")
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()
print("Car started")
if __name__ == "__main__":
my_car = Car()
my_car.start()
测试命令:
python3.12 ch01/composition.py
你应能看到如下输出:
Engine started
Car started
如本例所示,Car 类通过 self.engine = Engine() 组合了一个 Engine 对象,因此你可以轻松将引擎替换为另一种类型,而无需修改 Car 类本身。
遵循“面向接口而非实现(Program to Interfaces, Not Implementations)”原则
在软件设计中,人们很容易沉迷于功能“如何实现”的细枝末节。然而,过度关注实现细节会让代码高度耦合、难以修改。“面向接口而非实现编程”这一原则正是为解决此类问题而提出的。
这意味着什么?
接口为类定义了一种“契约”,规定了必须实现的一组方法。
该原则鼓励我们针对接口编程,而不是针对具体类编程。这样,你的代码就不会与提供特定行为的具体类绑定在一起,从而更易于在不影响系统其余部分的情况下替换或扩展实现。
好处
面向接口编程具有多项益处:
- 灵活性:可以在不修改调用方代码的前提下,轻松切换不同实现。
- 可维护性:将代码从具体实现中解耦,便于更新或替换组件。
- 可测试性:接口使单元测试更容易,因为在测试时可以轻松对接口进行 mock。
在 Python 中实现接口的技术
在 Python 中,实现“接口”的主要方式有两种:抽象基类(ABCs)与协议(Protocols) 。
抽象基类(ABCs)
abc 模块提供的 ABC 允许你定义抽象方法,任何具体(非抽象)子类都必须实现这些方法。
下面通过示例演示如何定义一个抽象类(用作接口)并使用它。
首先,按如下方式导入 ABC 类与 abstractmethod 装饰器:
from abc import ABC, abstractmethod
然后,定义接口类:
class MyInterface(ABC):
@abstractmethod
def do_something(self, param: str):
pass
接着,定义实现该接口的具体类;它继承自接口类,并为 do_something 提供实现:
class MyClass(MyInterface):
def do_something(self, param: str):
print(f"Doing something with: '{param}'")
加入测试代码:
if __name__ == "__main__":
MyClass().do_something("some param")
完整代码(ch01/abstractclass.py)如下:
from abc import ABC, abstractmethod
class MyInterface(ABC):
@abstractmethod
def do_something(self, param: str):
pass
class MyClass(MyInterface):
def do_something(self, param: str):
print(f"Doing something with: '{param}'")
if __name__ == "__main__":
MyClass().do_something("some param")
测试命令:
python3.12 ch01/abstractclass.py
预期输出:
Doing something with: 'some param'
至此,你已经了解了如何在 Python 中定义一个接口以及实现该接口的具体类。
协议(Protocols)
在 Python 3.8 中通过 typing 模块引入,协议提供了一种比 ABC 更灵活的方式,即结构化鸭子类型(structural duck typing) :只要对象“拥有某些属性或方法”,就被视为满足协议,而无需特定的继承关系。
与传统“鸭子类型”仅在运行时判定兼容性不同,结构化鸭子类型允许在“编译时”(静态类型检查阶段)进行类型检查——例如在 IDE 中即可提前发现类型错误,从而让程序更健壮、更易调试。
使用协议的关键优势在于:它关注对象能做什么,而不是对象是什么。换言之,“看起来像鸭子、叫起来像鸭子,它就是鸭子”,与其继承层次无关。这一点对动态类型语言 Python 尤为实用,因为对象的行为往往比其实际类型更重要。
例如,你可以定义一个需要 draw() 方法的 Drawable 协议。任何实现了该方法的类都隐式满足该协议,无需显式继承。
再看一个更简短的例子:定义一个需要 fly() 方法的 Flyer 协议:
from typing import Protocol
class Flyer(Protocol):
def fly(self) -> None:
...
到此为止!任何拥有 fly() 方法的类都被视为 Flyer,无论它是否显式继承自 Flyer。这让代码更通用、更可复用,也与前文“多用组合、少用继承”的原则相契合。稍后我们会看到协议的一个实际用例。
示例——不同类型的日志记录器(使用 ABCs)
我们用 ABC 创建一个日志接口,以适配不同的日志机制,实现如下:
导入所需内容:
from abc import ABC, abstractmethod
定义带有 log 方法的 Logger 接口:
class Logger(ABC):
@abstractmethod
def log(self, message: str):
pass
实现两个具体的日志记录器,分别输出到控制台和文件:
class ConsoleLogger(Logger):
def log(self, message: str):
print(f"Console: {message}")
class FileLogger(Logger):
def log(self, message: str):
with open("log.txt", "a") as f:
f.write(f"File: {message}\n")
定义一个使用日志器的函数:
def log_message(logger: Logger, message: str):
logger.log(message)
注意:该函数的第一个参数类型是 Logger,也即任一实现了 Logger 接口的具体类实例(ConsoleLogger 或 FileLogger)。
加入测试代码:
if __name__ == "__main__":
log_message(ConsoleLogger(), "A console log.")
log_message(FileLogger(), "A file log.")
完整代码(ch01/interfaces.py)如下:
from abc import ABC, abstractmethod
class Logger(ABC):
@abstractmethod
def log(self, message: str):
pass
class ConsoleLogger(Logger):
def log(self, message: str):
print(f"Console: {message}")
class FileLogger(Logger):
def log(self, message: str):
with open("log.txt", "a") as f:
f.write(f"File: {message}\n")
def log_message(logger: Logger, message: str):
logger.log(message)
if __name__ == "__main__":
log_message(ConsoleLogger(), "A console log.")
log_message(FileLogger(), "A file log.")
测试命令:
python3.12 ch01/interfaces.py
预期输出:
Console: A console log.
此外,在运行命令的目录下,会生成名为 log.txt 的文件,内容包含:
File: A file log.
如同 log_message 函数所示,你可以在无需修改函数本身的情况下,轻松在不同日志机制之间切换。
示例——仍是日志记录器,但改用 Protocols
我们用协议方式重写上述示例:
首先导入 Protocol:
from typing import Protocol
通过继承 Protocol 定义 Logger 接口:
class Logger(Protocol):
def log(self, message: str):
...
其余代码保持不变。完整代码(ch01/interfaces_bis.py)如下:
from typing import Protocol
class Logger(Protocol):
def log(self, message: str):
...
class ConsoleLogger:
def log(self, message: str):
print(f"Console: {message}")
class FileLogger:
def log(self, message: str):
with open("log.txt", "a") as f:
f.write(f"File: {message}\n")
def log_message(logger: Logger, message: str):
logger.log(message)
if __name__ == "__main__":
log_message(ConsoleLogger(), "A console log.")
log_message(FileLogger(), "A file log.")
使用我们定义的协议进行静态类型检查:
mypy ch01/interfaces_bis.py
预期输出:
Success: no issues found in 1 source file
运行程序:
python3.12 ch01/interfaces_bis.py
其输出与前一版本相同;同样会生成 log.txt 文件,并在终端打印:
Console: A console log.
这是正常的,因为我们只改变了接口的定义方式。协议带来的约束仅在静态检查阶段生效,运行时不会强制实施,因此不会改变代码的实际执行结果。
遵循“松散耦合(Loose Coupling)”原则
随着软件复杂度增长,组件之间的关系可能变得纠缠不清,导致系统难以理解、维护和扩展。“松散耦合”原则旨在缓解这一问题。
这意味着什么?
松散耦合是指尽量减少程序各部分之间的依赖。在松散耦合的系统中,组件彼此独立,并通过清晰定义的接口进行交互,这样当你修改某一部分时,不会轻易影响到其他部分。
好处
松散耦合带来多重优势:
- 可维护性:依赖更少,更易于更新或替换单个组件。
- 可扩展性:更容易向系统中添加新特性或新组件。
- 可测试性:独立组件更容易进行隔离测试,从而提升软件整体质量。
实现松散耦合的技术
实现松散耦合的两项主要技术是依赖注入(Dependency Injection)和观察者模式(Observer Pattern) 。
依赖注入让组件从外部获取其依赖,而非在内部自行创建,从而更易替换或 mock 这些依赖。观察者模式则允许对象发布其状态变化,让其他对象做出响应,而无需彼此紧密绑定。二者的共同目标都是减少组件间的相互依赖,使系统更加模块化、易于管理。
我们将在第 5 章《行为型设计模式》中详细讨论观察者模式。现在先通过一个示例理解如何使用依赖注入。
示例——消息服务(Message Service)
在 Python 中,可以通过依赖注入实现松散耦合。下面是一个涉及 MessageService 的简单示例。
首先,定义 MessageService 类:
class MessageService:
def __init__(self, sender):
self.sender = sender
def send_message(self, message):
self.sender.send(message)
如上所示,初始化该类时需传入一个 sender 对象;该对象具有 send 方法用于发送消息。
第二步,定义 EmailSender 类:
class EmailSender:
def send(self, message):
print(f"Sending email: {message}")
第三步,定义 SMSSender 类:
class SMSSender:
def send(self, message):
print(f"Sending SMS: {message}")
现在我们可以用 EmailSender 实例化 MessageService 并发送消息;也可以改为用 SMSSender。测试代码如下:
if __name__ == "__main__":
email_service = MessageService(EmailSender())
email_service.send_message("Hello via Email")
sms_service = MessageService(SMSSender())
sms_service.send_message("Hello via SMS")
完整代码(保存为 ch01/loose_coupling.py)如下:
class MessageService:
def __init__(self, sender):
self.sender = sender
def send_message(self, message: str):
self.sender.send(message)
class EmailSender:
def send(self, message: str):
print(f"Sending email: {message}")
class SMSSender:
def send(self, message: str):
print(f"Sending SMS: {message}")
if __name__ == "__main__":
email_service = MessageService(EmailSender())
email_service.send_message("Hello via Email")
sms_service = MessageService(SMSSender())
sms_service.send_message("Hello via SMS")
测试命令:
python3.12 ch01/loose_coupling.py
预期输出:
Sending email: Hello via Email
Sending SMS: Hello via SMS
在该示例中,MessageService 通过依赖注入与 EmailSender、SMSSender 保持松散耦合。这样就能在不修改 MessageService 类的情况下,轻松切换不同的发送机制。
总结
本书从开发者在编写可维护、灵活且健壮的软件时应遵循的基础设计原则入手。从封装变化到多用组合、面向接口编程,以及追求松散耦合,这些原则为任何 Python 开发者奠定了坚实的基础。
正如你所见,这些原则并非纯理论构想,而是能显著提升代码质量的实用准则。它们为接下来的内容铺垫了舞台:深入探讨指导面向对象设计的更专业的一组原则。
在下一章,我们将深入 SOLID 原则——这五条设计准则旨在使软件设计更易理解、更灵活且更易维护。