第十章 使用头等函数的设计模式

第十章 使用头等函数的设计模式

策略模式的实现案例

《设计模式》一书对策略模式的总结为定义一系列算法,将每个算法封装起来,并使它们可以互换。策略模式让算法的变化独立于使用它的客户端。

在电商领域,一个清晰的策略模式应用示例是:根据客户属性或订单商品的特征,为订单计算不同的折扣。假设某在线商店有以下折扣规则:

  • 忠诚积分达到 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__ 方法。 因此,对于无状态或简单状态的命令,函数足以替代单方法命令类;对于复杂需求,可调用对象 + 闭包提供了更灵活、更简洁的方案。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值