1、介绍
开篇探讨了设计模式在不同编程语言中的适用性及其演化,核心内容包括:
- 设计模式的起源与核心观点 - 由“四人帮”提出的23种经典设计模式(基于C++实现),旨在解决面向对象编程中的常见问题,但其作者强调语言特性直接影响模式的适用性(如Smalltalk/C++的特性支撑了书中模式,而过程式语言可能需要其他模式)。
- 语言特性对模式的颠覆性影响 - 在动态语言(如Python)中,某些模式(如迭代器模式)因语言内置支持(如生成器)变得冗余;另一些模式(如访问者模式)在支持多方法的语言(如CLOS)中重要性降低。
- 动态语言对经典模式的简化 - Peter Norvig指出,在具备动态特性(如高阶函数、多方法)的语言中,《设计模式》中约70%的模式(如策略模式、命令模式)可能被简化或无需显式实现,代码更简洁。
- 本章的核心目标
- 以策略模式、命令模式为例,展示如何用函数替代类实现相同功能,从而减少面向对象编程中的样板代码,提升可读性和灵活性。例如,Python的一等函数特性可直接替代策略模式中的类层次结构。
- 总结:设计模式并非普适铁律,其价值和实现方式高度依赖编程语言特性;动态语言通过高阶函数等特性可简化传统模式,推动更简洁的代码实践。
2、Case Study: Refactoring Strategy
策略模式是一个很好的例子,表明如果在 Python 中把函数作为一等对象来使用,设计模式的实现可以变得更简单。在接下来的部分,我们将使用《设计模式》一书中描述的 “经典” 结构来描述和实现策略模式。
1. Classic Strategy
什么是策略模式(Strategy Pattern)?
策略模式是一种行为设计模式,用于定义一组算法,将每个算法封装起来,使它们可以互换使用,同时使使用这些算法的客户端独立于算法的实现。
定义(Design Patterns 书中定义)
“定义一组算法,将每个算法封装起来,并使它们可以互换使用。策略模式允许算法独立于使用它的客户而变化。”
在本例中,策略模式用于在电子商务系统中计算折扣,具体折扣逻辑取决于不同的促销策略。
业务背景与问题描述
电子商务场景中,客户根据其属性和所购买商品可以享受不同的折扣策略。假设在线商店定义了以下折扣规则:
- 忠诚折扣(FidelityPromo):拥有 1000 积分或以上的客户,每次订单可享受 5% 的全局折扣。
- 批量折扣(BulkItemPromo):任意订单中,购买 20 个或更多同种商品的商品项,可享受 10% 的折扣。
- 大订单折扣(LargeOrderPromo):订单中不同商品种类数达到 10 种或以上,可享受 7% 的全局折扣。
每个订单只允许应用一种促销折扣。
UML 类图
根据策略模式的结构,以下是系统各组成部分的角色及其职责:
Order
(上下文):管理订单数据,并根据指定的折扣策略计算最终价格。Promotion
(策略接口):定义折扣算法的公共接口。- 具体策略(Concrete Strategies):
FidelityPromo
:忠诚折扣的计算逻辑。BulkItemPromo
:批量折扣的计算逻辑。LargeOrderPromo
:大订单折扣的计算逻辑。
示例代码讲解
以下代码展示了如何使用策略模式实现订单折扣计算:
1. 定义基本数据结构
这些数据类用于表示客户、订单项和订单的基本信息。
from collections.abc import Sequence
from decimal import Decimal
from typing import NamedTuple, Optional
class Customer(NamedTuple):
name: str
fidelity: int
class LineItem(NamedTuple):
product: str
quantity: int
price: Decimal
def total(self) -> Decimal:
return self.price * self.quantity
class Order(NamedTuple):
# 上下文类:管理订单数据和执行折扣运算逻辑
customer: Customer
cart: Sequence[LineItem]
promotion: Optional['Promotion'] = None
def total(self) -> Decimal:
"""计算订单总金额"""
totals = (item.total() for item in self.cart)
return sum(totals, start=Decimal(0))
def due(self) -> Decimal:
"""计算折扣后的最终支付金额"""
if self.promotion is None:
discount = Decimal(0)
else:
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self) -> str:
return f"<Order total: {self.total():.2f} due: {self.due():.2f}>"
2. 定义策略接口和具体策略
策略模式的核心在于:
- 定义一个抽象策略接口,供所有具体策略实现。
- 每个具体策略实现各自的折扣计算逻辑。
(1) 折扣策略接口
from abc import ABC, abstractmethod
class Promotion(ABC):
# 折扣策略的抽象基类
@abstractmethod
def discount(self, order: Order) -> Decimal:
"""返回折扣金额"""
(2) 各种具体策略
class FidelityPromo(Promotion):
"""忠诚折扣:客户积分达到 1000 或以上,享受 5% 折扣"""
def discount(self, order: Order) -> Decimal:
rate = Decimal('0.05')
if order.customer.fidelity >= 1000:
return order.total() * rate
return Decimal(0)
class BulkItemPromo(Promotion):
"""批量折扣:任意商品项数量达到 20 个或以上,享受 10% 折扣"""
def discount(self, order: Order) -> Decimal:
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
class LargeOrderPromo(Promotion):
"""大订单折扣:订单中不同商品种类达到 10 种及以上,享受 7% 折扣"""
def discount(self, order: Order) -> Decimal:
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
模块测试与运行示例
以下是如何应用折扣策略的使用示例:
测试案例代码
# 创建两个客户
joe = Customer('John Doe', 0) # 积分不足
ann = Customer('Ann Smith', 1100) # 累计积分 1100
# 定义购物车
cart = (
LineItem('banana', 4, Decimal('.5')),
LineItem('apple', 10, Decimal('1.5')),
LineItem('watermelon', 5, Decimal('5.00'))
)
# 应用不同的折扣策略
print(Order(joe, cart, FidelityPromo())) # 没有折扣
print(Order(ann, cart, FidelityPromo())) # 5% 忠诚折扣
banana_cart = (
LineItem('banana', 30, Decimal('.5')), # 批量购买香蕉
LineItem('apple', 10, Decimal('1.5'))
)
print(Order(joe, banana_cart, BulkItemPromo())) # 批量折扣
long_cart = tuple(LineItem(str(sku), 1, Decimal(1)) for sku in range(10)) # 10 种商品
print(Order(joe, long_cart, LargeOrderPromo())) # 大订单折扣
print(Order(joe, cart, LargeOrderPromo())) # 不满足大订单条件
输出结果如下:
<Order total: 42.00 due: 42.00>
<Order total: 42.00 due: 39.90>
<Order total: 30.00 due: 28.50>
<Order total: 10.00 due: 9.30>
<Order total: 42.00 due: 42.00>
2. Function-Oriented Strategy
将策略模式从基于类的实现简化为基于函数的实现。
示例代码解析
下面是用函数重构后的基于函数策略模式代码。主要内容包括:
Customer
和LineItem
的定义(订单有关的数据结构)。Order
类作为“上下文”,它调用传入的促销函数,并返回订单应付金额。- 三个具体的促销策略作为普通函数实现。
数据结构:Customer
和 LineItem
from collections.abc import Sequence
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional, Callable, NamedTuple
class Customer(NamedTuple):
name: str
fidelity: int # 客户的忠诚度积分
class LineItem(NamedTuple):
product: str # 商品名称
quantity: int # 商品购买数量
price: Decimal # 单价
def total(self): # 商品总价
return self.price * self.quantity
说明:
- 上述实现使用了
NamedTuple
,它既提供了类的结构,又具有元组的不可变性,适用于描述轻量结构的数据比如客户和订单项。 total
方法直接计算单件商品的总价。
核心结构:Order
@dataclass(frozen=True)
class Order:
customer: Customer # 关联顾客
cart: Sequence[LineItem] # 购物车商品列表
promotion: Optional[Callable[['Order'], Decimal]] = None # 折扣策略函数 接受一个 Order 对象作为参数,返回一个 Decimal 类型的值
def total(self) -> Decimal:
"""计算购物车总金额"""
totals = (item.total() for item in self.cart)
return sum(totals, start=Decimal(0))
def due(self) -> Decimal:
"""计算应付金额 = 总金额 - 折扣"""
if self.promotion is None:
discount = Decimal(0) # 没有折扣
else:
discount = self.promotion(self) # 调用折扣函数
return self.total() - discount
def __repr__(self):
"""输出订单规格总金额和应付金额"""
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
关键点:
promotion
是一个可调用对象(即函数)或者None
。- 计算折扣时直接调用传入的折扣函数(
self.promotion(self)
),并将Order
本身作为参数传递。 frozen=True
表示Order
是不可变的,增加了安全性,适用于订单这种容易被错误修改的对象。
促销策略实现为普通函数
下面的三种具体促销策略,简单且没有状态:
1. 基于客户忠诚度的促销
def fidelity_promo(order: Order) -> Decimal:
"""忠诚客户折扣:忠诚度积分 >= 1000, 折扣 5%"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
2. 批量商品折扣
def bulk_item_promo(order: Order) -> Decimal:
"""批量折扣:每件商品购买数量 >= 20, 折扣 10%"""
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
3. 大订单折扣
def large_order_promo(order: Order) -> Decimal:
"""大订单折扣:总商品种类 >= 10, 折扣 7%"""
distinct_items = {item.product for item in order.cart} # 利用集合,统计不同商品种类
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
示例使用(Order
测试)
下面是示例调用:
# 创建顾客
joe = Customer('John Doe', 0)
ann = Customer('Ann Smith', 1100)
# 购物车示例
cart = [LineItem('banana', 4, Decimal('.5')),
LineItem('apple', 10, Decimal('1.5')),
LineItem('watermelon', 5, Decimal(5))]
# 订单示例演示
print(Order(joe, cart, fidelity_promo)) # 无折扣:忠诚度积分不足
# 输出:<Order total: 42.00 due: 42.00>
print(Order(ann, cart, fidelity_promo)) # 折扣 5%
# 输出:<Order total: 42.00 due: 39.90>
banana_cart = [LineItem('banana', 30, Decimal('.5')),
LineItem('apple', 10, Decimal('1.5'))]
print(Order(joe, banana_cart, bulk_item_promo)) # 批量折扣
# 输出:<Order total: 30.00 due: 28.50>
long_cart = [LineItem(str(item_code), 1, Decimal(1))
for item_code in range(10)]
print(Order(joe, long_cart, large_order_promo)) # 大订单折扣
# 输出:<Order total: 10.00 due: 9.30>
注意点和扩展示例
注意点:
Why
self.promotion(self)
?因为
promotion
是一个函数,而不是方法,Python 不会自动绑定self
,因此需要主动显式传入上下文(Order
实例本身)。类型提示更清晰:
Optional[Callable[['Order'], Decimal]]
明确表示promotion
接受一个函数,该函数输入Order
返回Decimal
。
对比示例:类 vs 函数策略
类的策略使用示例(更繁琐):
promotion = FidelityPromo() order = Order(customer, cart, promotion) result = order.due()
函数策略使用示例(更简洁):
order = Order(customer, cart, fidelity_promo) result = order.due()
关于self.promotion(self)
假定我们有以下代码:
order = Order(ann, cart, fidelity_promo)
discount = order.due() # 内部调用 self.promotion(self)
第一步:创建 Order
实例
Order
类实例order
被创建时,self
是这个实例的指针,包含以下三个主要属性:self.customer
指向了ann
(Customer
实例)。self.cart
是存储了商品的列表。self.promotion
存储了函数fidelity_promo
。
内存示意: order ──> [Customer, Cart, Promotion] / / \ ann cart fidelity_promo
第二步:调用 order.due()
order.due()
方法中会调用self.promotion(self)
。此时
self.promotion
是一个已绑定的fidelity_promo
函数。self
是order
实例本身。内存内部的表现:
self.promotion --> fidelity_promo fidelity_promo --> 函数对象(代码 + 参数规则 + 指向作用域) self (传递给函数) --> order 实例
第三步:执行 fidelity_promo(self)
在
fidelity_promo(self)
调用中:self
参数被绑定到order
实例。- 函数访问了传入的
order
数据,执行了order.total()
和相关的逻辑(忠诚度积分检查等)。 - 函数返回了处理后的折扣值。
当函数返回时,折扣被用于计算总应付金额。
内存更新: order ───> fidelity_promo(order) 返回 Decimal('2.10')
self
与 Order
的关系
self
是在方法内部使用的语法糖
self
是一个名字,用来在类的方法中引用当前的实例(也就是Order
实例本身)。- 外部调用时,我们更常显式地使用实例名(如
order.total()
),但在类的方法内,我们用self
来引用同一个实例。
外部的 Order
映射到内部的 self
- 如果在外部创建了一个
Order
实例,如order = Order(...)
,在调用实例的方法(如order.due()
)时,Python 会自动把order
作为第一个参数传递给方法体,而这个参数在方法内部被命名为self
。
3. Choosing the Best Strategy: Simple Approach
对于一种应用场景,顾客在结算购物车时可能符合多个折扣条件,例如:
- 基于顾客忠诚度的折扣(
fidelity_promo
)。 - 针对大批量单项商品的折扣(
bulk_item_promo
)。 - 针对大额订单的折扣(
large_order_promo
)。
我们的任务是:
- 收集所有可能的折扣策略。
- 计算给定订单的最佳折扣(即最大折扣)。
针对这个需求,我们将实现一个函数 best_promo
,它会动态地选择“最优折扣方案”。
原代码实现与分析
从原代码中,通过“设计模式”实现了灵活的折扣机制。
示例代码及结果
# promos: 存储所有折扣策略的列表
promos = [fidelity_promo, bulk_item_promo, large_order_promo]
def best_promo(order: Order) -> Decimal:
"""计算订单可用的最大折扣"""
return max(promo(order) for promo in promos)
测试实例
以下是三个测试场景的运行结果:
>>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30>
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>
>>> Order(ann, cart, best_promo)
<Order total: 42.00 due: 39.90>
运行逻辑分析
promos
列表
promos
是一个函数列表,存储了所有已经实现的折扣策略函数,如fidelity_promo
,bulk_item_promo
, 和large_order_promo
。- 这种方式展示了函数作为“一等对象”的强大特性:函数可以像变量一样存储在列表中,稍后动态调用。
best_promo
函数- 对于给定订单
order
,函数逐一调用promos
列表中的每个折扣策略函数promo
。 - 使用 max 函数获取所有折扣值中的最大值。
- 对于给定订单
代码优劣分析
优点:- 灵活性:折扣策略是独立函数,彼此隔离,功能清晰。
- 易扩展性:可以轻松添加或替换折扣策略。
潜在问题:
- 不确定性: 如果忘记将新折扣策略添加到
promos
列表,best_promo
无法动态选择新策略。 - 重复性: 每次添加新折扣策略,不仅需要实现策略函数,还必须同时更新
promos
列表,增加了维护成本,尤其在折扣策略较多时。
4. Finding Strategies in a Module
本文利用动态 introspection 技术和模块全局命名空间,来实现一种“自动化”的策略函数收集方式。以下以代码示例为主,同时详细讲解其工作原理和潜在问题。
利用 globals()
寻找策略
示例代码 10-7:利用 globals()
寻找策略
globals()
是一个内置函数,用于获取当前模块的全局符号表。它返回一个词典,其中键是符号名,值是符号对应的对象。我们可以利用它自动组织模块中的函数或变量。
以下是代码示例:
from decimal import Decimal
from strategy import Order
from strategy import (
fidelity_promo, # 针对忠诚客户的优惠
bulk_item_promo, # 批量商品的优惠
large_order_promo # 大订单的优惠
)
# 策略函数的动态搜索
promos = [
promo for name, promo in globals().items()
if name.endswith('_promo') and name != 'best_promo'
]
def best_promo(order: Order) -> Decimal:
"""计算可用的最佳折扣"""
return max(promo(order) for promo in promos)
代码详解
globals()
解析模块全局命名空间:globals()
返回当前模块的全局定义表(即变量、函数等),是一个字典。- 每个全局符号的名字作为键,对应的对象作为值。
- 示例中,我们通过字典解析
globals().items()
遍历所有全局定义。
筛选符合条件的函数对象:
- 筛选名称以
_promo
结尾的对象(符合策略命名约定)。 - 排除
best_promo
函数本身,防止循环调用造成无限递归错误。
- 筛选名称以
自动化函数集合:
- 提前显式导入所有策略函数。
- 使用列表推导式,根据命名约定动态收集这些函数,加入到
promos
列表。
best_promo
的作用:- 遍历所有策略函数,计算当前订单的最大折扣。
- 使用
max()
比较所有策略返回的结果。
运行结果:
如果模块中定义了以下策略函数:
def fidelity_promo(order):
return Decimal('5.00')
def bulk_item_promo(order):
return Decimal('10.00')
调用 best_promo
:
order = Order(...) # 示例订单
print(best_promo(order))
输出最终返回计算的最大折扣(例如 10.00
)。
优缺点分析:
优点:
- 自动化:开发者只需按命名规范创建新策略函数,无需显式维护一个策略函数的集合(如列表)。
- 易扩展:添加新策略后,不需要修改代码的其他部分,代码具有较好的可维护性。
缺点:
- 命名耦合:函数名必须遵守命名规范(
_promo
),否则无法被动态收集。 - 隐式约定:代码隐含“所有匹配
_promo
的函数都必须具备同样的签名”,否则可能因调用错误函数导致崩溃。
- 命名耦合:函数名必须遵守命名规范(
补充示例:小心命名冲突
def best_promo():
return "invalid call"
# 如果无意中定义了与逻辑相关的 _promo 函数,globals() 的解析逻辑可能破坏原始设计。
# 见下方调用:
print(globals())
分析结果表明,在大型工程团队中,可能无意引入非策略相关的 _promo
函数,导致意外行为。
解决方法: 可改用更明确的装饰器方法,实现约定的绑定(将在下面介绍)。
inspect
模块 introspection 子模块
示例代码 10-8:通过 inspect
模块 introspection 子模块
inspect
提供更丰富的 introspection 方法,可精确过滤需要的函数。
以下是代码示例:
from decimal import Decimal
import inspect
from strategy import Order
import promotions
# introspection 遍历 promotions 模块中的所有函数
promos = [
func for _, func in inspect.getmembers(
promotions, inspect.isfunction
)
]
def best_promo(order: Order) -> Decimal:
"""计算可用的最佳折扣"""
return max(promo(order) for promo in promos)
代码详解
inspect.getmembers()
的作用:getmembers
获取一个对象的所有属性及其对应值。- 参数:
- 第一个参数:要 introspect 的对象(这里是
promotions
模块)。 - 第二个参数:可选谓词函数,用于筛选满足条件的属性(这里是
inspect.isfunction
,表示只关心函数)。
- 第一个参数:要 introspect 的对象(这里是
通过这种方式,我们可以从指定模块
promotions
中精准找到所有属于自定义策略类的函数。收集函数的特点:
- 相比
globals()
方法,inspect
方法避免了依赖命名约定。 - 直观地通过模块隔离,减少因命名冲突带来的不必要错误。
- 相比
运行结果:
如果 promotions
子模块包含函数如下:
# promotions.py
def fidelity_promo(order):
return Decimal('5.00')
def bulk_item_promo(order):
return Decimal('10.00')
运行:
order = Order(...)
print(best_promo(order))
输出:返回匹配订单的最大折扣(如 10.00
)。
优缺点分析:
优点:
- 抽离模块与核心函数逻辑:解耦命名冲突问题,
promotions
模块专布函数。 - 易扩展:通过将策略函数集中到单独模块,能够更直观管理这些逻辑。
- 抽离模块与核心函数逻辑:解耦命名冲突问题,
缺点:
- 隐式依赖:代码假定
promotions
模块中的函数均满足固定的接口设计(相同的函数参数和返回值类型)。
- 隐式依赖:代码假定
3、Decorator-Enhanced Strategy Pattern
动机与问题描述
在传统的策略模式实现中,实际业务需求可能导致代码里的逻辑存在“重复”的问题。
比如:在我们需要实现多种促销策略时,不同的策略需要单独定义函数,并且还需要手动将这些函数添加到促销策略列表中。
然而,这种重复和手动维护逻辑很容易出错。如果新增一个促销策略时,开发者忘记将其添加到促销列表里,可能导致该促销策略被忽略,从而引入潜在的隐性 bug。
核心问题:
- 代码重复性:函数定义后需要再手动添加到列表中。
- 易出错:新增或修改策略时可能忘记更新相应的促销策略列表,导致问题难以发现。
解决方案简介
为了解决上述问题,我们可以借助装饰器(decorator)。
装饰器可以在函数定义和调用时加入额外的逻辑,比如自动将促销函数注册到促销列表中。这种实现方法清晰、简洁且不易出错,是 Python 中函数式编程的经典应用。
代码实现:使用装饰器改进策略模式
以下将基于原示例详细剖析和讲解代码逻辑。
# 定义类型别名,方便类型标注
Promotion = Callable[[Order], Decimal] # Promotion 是接收 Order 返回 Decimal 类型
promos: list[Promotion] = [] # 保存所有促销策略的全局列表,用于动态注册促销函数
# 装饰器定义,用于自动注册促销策略
def promotion(promo: Promotion) -> Promotion:
"""
装饰器:将被装饰的促销策略函数注册到 promos 列表中
"""
promos.append(promo)
return promo
Promotion = Callable[[Order], Decimal]
的理解:
类型别名用赋值操作:
使用赋值语法Name = SomeType
创建一个类型别名,这是一种简便方式,用来省略重复书写复杂类型提示的麻烦。具体示例:Promotion = Callable[[Order], Decimal] # 创建类型别名
类型标注用冒号
:
:
使用冒号为变量、函数参数或返回值做类型声明。例如:def apply_promotion(order: Order, func: Promotion) -> Decimal: ...
两者结合使用:我们可以先用赋值语法定义一个类型别名,然后利用
:
来标注类型,使代码既简洁清晰又方便维护。
promotion
函数的作用:
- 核心功能:将被装饰的促销策略函数自动加入到
promos
列表中,无需手动维护。 - 设计思路:返回的
promo
函数不被改变,装饰器仅增加一个附加效果(注册到列表)。 - 优点:避免了遗漏新增策略的风险,引入了自动化注册机制。
装饰器的本质:
- 装饰器接受一个函数作为参数,并返回一个函数(可以是修改过的版本,也可以不)。
- 在这个例子中,
promotion
是一个装饰器,它接收一个函数promo
,将其注册到promos
列表中后,直接返回这个函数本身。
装饰器的目的:
- 你可以把装饰器看作“包装”函数,可以在函数的定义时动态增加一些功能。
- 这里的装饰器
promotion
的功能是 动态注册促销策略函数 (收集所有被装饰的函数),它直接将函数添加到全局的promos
列表中,非常便于管理。
简化形式:
- 这个装饰器并没有嵌套函数,因为它没有对函数的行为做任何修改——它只是将函数注册进列表。
- 因此,它可以简化为直接返回的形式:
return promo
,不需要定义内部wrapper
函数。
解释具体的运行流程:
当你使用
@promotion
装饰fidelity
函数时:- 函数
fidelity
会被传递给装饰器promotion
。 fidelity
会被添加到promos
列表中。- 装饰器返回“未修改的原始函数”,所以
fidelity
原本的行为没有任何改变。
- 函数
最终,
promos
会包含所有使用了@promotion
装饰器的函数。这样就可以很方便地动态实现某些功能(比如批量对promos
列表中的函数进行操作)。
使用装饰器定义促销策略
接下来,我们定义多种促销策略,并通过 @promotion
装饰器注册它们:
@promotion
def fidelity(order: Order) -> Decimal:
"""5% 折扣:适用于具有 1000 或更多忠诚点的客户"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
@promotion
def bulk_item(order: Order) -> Decimal:
"""10% 折扣:适用于购物车中,商品数量达到 20 或更多的情况"""
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
@promotion
def large_order(order: Order) -> Decimal:
"""7% 折扣:适用于购物车中包含 10 种或更多不同商品的订单"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
特点
- 每个函数通过
@promotion
装饰后,自动注册到promos
列表中,开发者无需手动维护。 - 折扣规则逻辑保持清晰独立,易于阅读和扩展。
优化后的最佳促销计算
我们使用以下函数来选择最优折扣策略:
def best_promo(order: Order) -> Decimal:
"""
计算当前订单可用的最佳折扣
"""
return max(promo(order) for promo in promos)
功能说明
- 遍历
promos
列表,依次应用每种折扣策略。 - 通过内置函数
max
,选取对order
来说折扣力度最大的值。 - 优点:无需修改
best_promo
函数。当我们新增或移除一个促销策略时,promos
列表会动态更新。
完整案例
from collections.abc import Sequence
from dataclasses import dataclass
from decimal import Decimal
from typing import Optional, Callable, NamedTuple
class Customer(NamedTuple):
name: str
fidelity: int # 客户的忠诚度积分
class LineItem(NamedTuple):
product: str # 商品名称
quantity: int # 商品购买数量
price: Decimal # 单价
def total(self): # 商品总价
return self.price * self.quantity
@dataclass(frozen=True)
class Order:
customer: Customer # 关联顾客
cart: Sequence[LineItem] # 购物车商品列表
promotion: Optional[Callable[['Order'], Decimal]] = None # 折扣策略函数 接受一个 Order 对象作为参数,返回一个 Decimal 类型的值
def total(self) -> Decimal:
"""计算购物车总金额"""
totals = (item.total() for item in self.cart)
return sum(totals, start=Decimal(0))
def due(self) -> Decimal:
"""计算应付金额 = 总金额 - 折扣"""
if self.promotion is None:
discount = Decimal(0) # 没有折扣
else:
discount = self.promotion(self) # 调用折扣函数
return self.total() - discount
def __repr__(self):
"""输出订单规格总金额和应付金额"""
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
# 定义类型别名,方便类型标注
Promotion = Callable[[Order], Decimal] # Promotion 是接收 Order 返回 Decimal 类型
promos: list[Promotion] = [] # 保存所有促销策略的全局列表,用于动态注册促销函数
# 装饰器定义,用于自动注册促销策略
def promotion(promo: Promotion) -> Promotion:
"""
装饰器:将被装饰的促销策略函数注册到 promos 列表中
"""
promos.append(promo)
return promo
@promotion
def fidelity(order: Order) -> Decimal:
"""5% 折扣:适用于具有 1000 或更多忠诚点的客户"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
@promotion
def bulk_item(order: Order) -> Decimal:
"""10% 折扣:适用于购物车中,商品数量达到 20 或更多的情况"""
discount = Decimal(0)
for item in order.cart:
if item.quantity >= 20:
discount += item.total() * Decimal('0.1')
return discount
@promotion
def large_order(order: Order) -> Decimal:
"""7% 折扣:适用于购物车中包含 10 种或更多不同商品的订单"""
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
def best_promo(order: Order) -> Decimal:
"""
计算当前订单可用的最佳折扣
"""
return max(promo(order) for promo in promos)
# Customer、LineItem 和 Order 模拟实例
john = Customer(name="giao", fidelity=1200)
cart = [
LineItem('apple', 10, Decimal('1.50')),
LineItem('banana', 30, Decimal('0.80')),
LineItem('pear', 5, Decimal('1.00'))
]
order = Order(customer=john, cart=cart)
for promo in promos:
print(f"{promo.__name__}: {promo(order)}")
print(f"Best discount: {best_promo(order)}")
输出结果:
fidelity: 2.20 # 1200 忠诚点积分 -> 5% 折扣
bulk_item: 2.40 # 超过 20 数量的香蕉商品 -> 10% 折扣
large_order: 0.00 # 不满足 10 种不同商品
Best discount: 2.40
如何禁用某些促销策略
如果我们想临时禁用某种促销策略,只需注释掉对应函数的装饰器:
# 注释掉以下装饰器,该策略将从 promos 列表中移除
# @promotion
def fidelity(order: Order) -> Decimal:
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
对比传统方法与装饰器方法
特点 | 传统策略模式 | 装饰器增强策略模式 |
---|---|---|
新增策略的易用性 | 需手动添加函数至列表 | 使用装饰器自动注册 |
潜在错误风险 | 忘记更新列表可能导致策略未生效 | 无需手动维护列表,几乎零风险 |
可读性和扩展性 | 策略列表与逻辑分离,维护不直观 | 逻辑清晰,扩展性强 |
注释或禁用单一策略的难度 | 需修改多个地方 | 注释装饰器即可 |
4、The Command Pattern
一、命令模式简介
命令模式目的
命令模式的核心目标是解耦**调用操作的对象(调用者,Invoker)和实现操作的对象(接收者,Receiver)**之间的关系。
在典型的命令模式中:
- 调用者仅需要知道命令对象的接口(如
execute()
);它不需要关心操作的细节。 - 不同的命令对象将指向不同的接收者并执行各自的操作。
常见应用场景
- 图形用户界面(GUI):实现菜单操作。
- 支持宏录制(Macro Command)或撤销(Undo)操作。
命令模式中的主要角色
Invoker(调用者)
发起命令请求的对象。在例子中是Menu
的实例。Command(命令接口)
定义了命令的接口方法(如execute()
)。通过具体的命令类来实现该接口。Concrete Commands(具体命令类)
- 如
OpenCommand
,PasteCommand
等。 - 每个具体命令类都绑定一个接收者,执行特定操作。
- 如
Receiver(接收者)
实现实际执行逻辑的对象,例如Document
或Application
。Client(客户代码)
将具体命令对象与调用者关联。
二、命令模式的具体实现
示例 UML(见上图)
说明:Menu
是调用者,负责调用绑定的命令。- 每个命令都有自己的接收者。
MacroCommand
可以存储多个命令,并按照顺序依次调用它们。
基本代码实现:
传统命令模式:class Command: def execute(self): pass # 接收者类 class Document: def insert_text(self, text): print(f"Text '{text}' has been inserted into the document.") # 具体命令类 class PasteCommand(Command): def __init__(self, receiver, text): self.receiver = receiver self.text = text def execute(self): self.receiver.insert_text(self.text) # 调用者类 class Menu: def __init__(self): self.command = None def set_command(self, command): self.command = command def click(self): if self.command: self.command.execute() else: print("No command set.") # 客户端代码 document = Document() paste_command = PasteCommand(document, "Hello, World!") menu = Menu() menu.set_command(paste_command) menu.click() # 输出: Text 'Hello, World!' has been inserted into the document.
三、改进:使用函数代替具体命令类
Python 的一等函数特性允许更简洁地实现命令模式,用函数代替单一方法的命令类。示例如下:
# 调用者类
class Menu:
def __init__(self):
self.command = None # 绑定的命令
def set_command(self, command):
"""设置命令"""
self.command = command
def click(self):
"""模拟用户点击"""
if self.command:
self.command()
else:
print("No command set.")
# 示例:普通函数作为命令
def paste_function(text):
print(f"Text '{text}' has been inserted.")
# 宏命令类
class MacroCommand:
"""宏命令执行一系列命令"""
def __init__(self, commands):
self.commands = list(commands) # 保留传入的命令列表
def __call__(self): # 让对象像函数一样调用
"依次调用每个命令"
for command in self.commands:
command()
# 示例运行
def open_file():
print("Opening the file...")
def paste_text():
print("Pasting text...")
if __name__ == "__main__":
# 独立命令示例
menu = Menu()
menu.set_command(lambda: paste_function("Hello, World!"))
menu.click()
# 输出: Text 'Hello, World!' has been inserted.
print("---")
# 宏命令示例
macro = MacroCommand([open_file, paste_text])
macro()
# 输出:
# Opening the file...
# Pasting text...
优点:
- 简化 Command 的实现,将重点放在函数设计上。
- 减少样板代码(如定义一个只含一个方法的类)。
5、Chapter Summary
正如彼得·诺维格(Peter Norvig)在经典的《设计模式》一书出版几年后所指出的:“在23种设计模式中,至少在某些应用场景下,有16种模式在Lisp或Dylan语言中的实现,在本质上比在C++中更简单”(诺维格《动态语言中的设计模式》演讲的第9页幻灯片)。Python具备Lisp和Dylan语言的一些动态特性,尤其是一等函数,这也是本书这一部分的重点内容。 在本章开头引用的同一场演讲中,拉尔夫·约翰逊(Ralph Johnson)在回顾《设计模式:可复用的面向对象软件元素》出版20周年时表示,这本书的缺点之一在于 “过于强调将设计模式作为终点,而不是设计过程中的步骤”。在本章中,我们以策略模式为起点,展示了如何利用一等函数简化现有解决方案。 在许多情况下,在Python中,使用函数或可调用对象来实现回调,比模仿伽玛(Gamma)、海尔姆(Helm)、约翰逊(Johnson)和弗里斯ides(Vlissides)在《设计模式》一书中描述的策略模式或命令模式更为自然。本章中对策略模式的重构以及对命令模式的讨论,体现了一个更普遍的观点:有时你可能会遇到一种设计模式或API,要求组件实现一个只有单一方法的接口,而且这个方法的名称听起来很通用,比如 “execute”(执行)、“run”(运行)或 “do_it”(做这件事)。在Python中,将函数作为一等对象使用,往往可以用更少的样板代码来实现这类模式或API。
在我们合作对本书进行最后的润色时,技术审稿人莱昂纳多・罗查尔提出了一个疑问:
如果函数有__call__
方法,并且方法也是可调用的,那么__call__
方法本身也有__call__
方法吗?
我不知道他的这个发现是否有用,但这确实是个有趣的现象:
>>> def turtle():
... return 'eggs'
...
>>> turtle()
'eggs'
>>> turtle.__call__()
'eggs'
>>> turtle.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__.__call__()
'eggs'
>>> turtle.__call__.__call__.__call__.__call__.__call__.__call__.__call__()
'eggs'
层层嵌套,无穷无尽。