第十章 使用头等函数的设计模式
策略模式的实现案例
《设计模式》一书对策略模式的总结为定义一系列算法,将每个算法封装起来,并使它们可以互换。策略模式让算法的变化独立于使用它的客户端。
在电商领域,一个清晰的策略模式应用示例是:根据客户属性或订单商品的特征,为订单计算不同的折扣。假设某在线商店有以下折扣规则:
- 忠诚积分达到 1,000 或以上的客户,每笔订单享受 5% 的全局折扣。
- 同一订单中,任一商品数量达到 20 件或以上,该商品享受 10% 的折扣。
- 订单中包含至少 10 种不同商品,则整单享受 7% 的全局折扣。
经典面向对象实现
from abc import ABC, abstractmethod
from collections.abc import Sequence
from decimal import Decimal
from typing import NamedTuple, Optional
# 客户信息:使用 NamedTuple 定义不可变客户对象
class Customer(NamedTuple):
name: str # 客户姓名
fidelity: int # 忠诚积分
# 订单中的商品项:每个商品包含名称、数量和单价
class LineItem(NamedTuple):
product: str # 商品名称
quantity: int # 数量
price: Decimal # 单价(使用 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) # 无促销策略,折扣为 0
else:
# 调用策略对象的 discount 方法,传入当前订单实例
discount = self.promotion.discount(self)
return self.total() - discount
def __repr__(self):
"""自定义字符串表示,便于调试和打印"""
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
# 抽象基类:定义所有促销策略必须实现的接口
class Promotion(ABC):
@abstractmethod
def discount(self, order: Order) -> Decimal:
"""返回应给予该订单的折扣金额(正数)"""
pass
# 具体策略 1:忠诚客户折扣
class FidelityPromo(Promotion):
"""为忠诚积分达到 1000 或以上的客户,提供 5% 的全局折扣"""
def discount(self, order: Order) -> Decimal:
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
# 具体策略 2:大数量商品折扣
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
# 具体策略 3:大订单折扣
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)
from decimal import Decimal
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, FidelityPromo())) # <Order total: 42.00 due: 42.00>
print(Order(ann, cart, FidelityPromo())) # <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, BulkItemPromo())) # <Order total: 30.00 due: 28.50>
long_cart = tuple(LineItem(str(sku), 1, Decimal(1)) for sku in range(10))
print(Order(joe, long_cart, LargeOrderPromo())) # <Order total: 10.00 due: 9.30>
print(Order(joe, cart, LargeOrderPromo())) # <Order total: 42.00 due: 42.00>
面向函数的重构
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) -> Decimal:
return self.price * self.quantity
# Order 不再依赖抽象基类 Promotion
# 使用 @dataclass(frozen=True) 替代 NamedTuple
# promotion 字段类型为 Optional[Callable[['Order'], Decimal]],
# 表示它可以是 None,也可以是一个接受 Order 实例、返回 Decimal 的函数。
@dataclass(frozen=True)
class Order:
customer: Customer
cart: Sequence[LineItem]
# 不再是 Promotion 子类实例,而是函数
promotion: Optional[Callable[['Order'], Decimal]] = 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:
# 直接调用函数,而非方法
# self.promotion 是一个普通函数(非绑定方法),因此需显式传入 self
discount = self.promotion(self)
return self.total() - discount
def __repr__(self):
return f'<Order total: {self.total():.2f} due: {self.due():.2f}>'
# 策略由函数实现,而非类
# 每个策略现在是一个独立的顶层函数,无状态、无类结构
def fidelity_promo(order: Order) -> Decimal:
"""忠诚积分 ≥1000 的客户享受 5% 折扣"""
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
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
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)
from decimal import Decimal
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)) # <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>
print(Order(joe, cart, large_order_promo)) # <Order total: 42.00 due: 42.00>
自动选择最优策略
显式维护函数列表
promos = [fidelity_promo, bulk_item_promo, large_order_promo]
# 应用所有折扣并返回最大值
# 清晰可控 但是容易忘了更新
def best_promo(order: Order) -> Decimal:
return max(promo(order) for promo in promos)
通过 globals() 自动发现
# 依赖命名约定(_promo 后缀)还需要排除 `best_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)
通过模块内省
# 不依赖命名,但假设模块中所有函数都是策略函数。
# 更灵活,但需注意模块污染风险
import inspect
import 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)
装饰器增强的策略模式
策略模式的自动注册机制
在显示维护函数列表案例中,promos 列表需手动维护,容易因遗漏新策略函数而导致 best_promo 无法使用最新策略,形成隐蔽 bug。
通过定义 @promotion 装饰器,在函数定义时自动将其注册到全局策略列表中,消除手动维护负担。
from decimal import Decimal
from typing import Callable
from strategy import Order
# 类型别名:策略函数接受 Order,返回 Decimal
Promotion = Callable[[Order], Decimal]
promos: list[Promotion] = [] # 全局策略注册表
def promotion(promo: Promotion) -> Promotion:
"""注册装饰器:将策略函数加入 promos 列表"""
promos.append(promo)
return promo # 原样返回函数,不改变其行为
def best_promo(order: Order) -> Decimal:
"""计算所有可用策略中的最大折扣"""
return max(promo(order) for promo in promos)
# 使用 @promotion 装饰器自动注册
@promotion
def fidelity(order: Order) -> Decimal:
if order.customer.fidelity >= 1000:
return order.total() * Decimal('0.05')
return Decimal(0)
@promotion
def bulk_item(order: Order) -> Decimal:
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:
distinct_items = {item.product for item in order.cart}
if len(distinct_items) >= 10:
return order.total() * Decimal('0.07')
return Decimal(0)
使用注册表的优势在于无需特殊命名约定(如 _promo 后缀)。装饰器显式声明意图,@promotion 清晰表明该函数是策略。易于启用/禁用,注释掉装饰器即可临时移除策略。模块无关性,策略函数可定义在任意模块,只要应用了 @promotion。核心思想在于利用 Python 的装饰器机制实现自动注册,将“定义”与“发现”合二为一。
命令模式的函数式重构
经典命令模式
命令模式的目的是解耦调用者(Invoker) 与接收者(Receiver)。通过 Command 对象封装操作,调用者仅调用 command.execute()。
函数式替代方案
在 Python 中,函数本身就是可调用对象,天然可作为“命令”使用:调用者无需调用 command.execute(),直接调用 command() 即可。对于需要状态或组合的场景,可使用可调用类(实现 __call__ 方法)。
class MacroCommand:
"""一个可调用的命令容器,按顺序执行多个命令"""
def __init__(self, commands):
# 保存命令列表的副本,避免外部修改影响
self.commands = list(commands)
def __call__(self):
# 当实例被调用时,依次执行所有命令
for command in self.commands:
command()
def open_file():
print("Opening file...")
def save_file():
print("Saving file...")
macro = MacroCommand([open_file, save_file])
macro() # 输出: Opening file... \n Saving file...
# 如果需要支持撤销 可以使用闭包或者可调用类保存状态
def make_undoable(action, undo_action):
def command():
action()
command.undo = undo_action # 动态附加 undo 方法
return command
每个 Python 可调用对象(函数、实现了 __call__ 的类实例等)本质上都实现了单一方法接口——即 __call__ 方法。 因此,对于无状态或简单状态的命令,函数足以替代单方法命令类;对于复杂需求,可调用对象 + 闭包提供了更灵活、更简洁的方案。
9979

被折叠的 条评论
为什么被折叠?



