CHAPTER 10 Design Patterns with First-Class Functions

发布于:2025-03-25 ⋅ 阅读:(41) ⋅ 点赞:(0)

1、介绍

开篇探讨了设计模式在不同编程语言中的适用性及其演化,核心内容包括:

  1. 设计模式的起源与核心观点 - 由“四人帮”提出的23种经典设计模式(基于C++实现),旨在解决面向对象编程中的常见问题,但其作者强调语言特性直接影响模式的适用性(如Smalltalk/C++的特性支撑了书中模式,而过程式语言可能需要其他模式)。
  2. 语言特性对模式的颠覆性影响 - 在动态语言(如Python)中,某些模式(如迭代器模式)因语言内置支持(如生成器)变得冗余;另一些模式(如访问者模式)在支持多方法的语言(如CLOS)中重要性降低。
  3. 动态语言对经典模式的简化 - Peter Norvig指出,在具备动态特性(如高阶函数、多方法)的语言中,《设计模式》中约70%的模式(如策略模式、命令模式)可能被简化或无需显式实现,代码更简洁。
  4. 本章的核心目标
    • 以策略模式、命令模式为例,展示如何用函数替代类实现相同功能,从而减少面向对象编程中的样板代码,提升可读性和灵活性。例如,Python的一等函数特性可直接替代策略模式中的类层次结构。
    • 总结:设计模式并非普适铁律,其价值和实现方式高度依赖编程语言特性;动态语言通过高阶函数等特性可简化传统模式,推动更简洁的代码实践。

2、Case Study: Refactoring Strategy

策略模式是一个很好的例子,表明如果在 Python 中把函数作为一等对象来使用,设计模式的实现可以变得更简单。在接下来的部分,我们将使用《设计模式》一书中描述的 “经典” 结构来描述和实现策略模式。

1. Classic Strategy

什么是策略模式(Strategy Pattern)?

策略模式是一种行为设计模式,用于定义一组算法,将每个算法封装起来,使它们可以互换使用,同时使使用这些算法的客户端独立于算法的实现。

定义(Design Patterns 书中定义)

“定义一组算法,将每个算法封装起来,并使它们可以互换使用。策略模式允许算法独立于使用它的客户而变化。”

在本例中,策略模式用于在电子商务系统中计算折扣,具体折扣逻辑取决于不同的促销策略。

业务背景与问题描述

电子商务场景中,客户根据其属性和所购买商品可以享受不同的折扣策略。假设在线商店定义了以下折扣规则:

  1. 忠诚折扣(FidelityPromo):拥有 1000 积分或以上的客户,每次订单可享受 5% 的全局折扣。
  2. 批量折扣(BulkItemPromo):任意订单中,购买 20 个或更多同种商品的商品项,可享受 10% 的折扣。
  3. 大订单折扣(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

将策略模式从基于类的实现简化为基于函数的实现。

示例代码解析

下面是用函数重构后的基于函数策略模式代码。主要内容包括:

  1. CustomerLineItem 的定义(订单有关的数据结构)。
  2. Order 类作为“上下文”,它调用传入的促销函数,并返回订单应付金额。
  3. 三个具体的促销策略作为普通函数实现。

数据结构:CustomerLineItem

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 函数策略

  1. 类的策略使用示例(更繁琐):

    promotion = FidelityPromo()
    order = Order(customer, cart, promotion)
    result = order.due()
    
  2. 函数策略使用示例(更简洁):

    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 是这个实例的指针,包含以下三个主要属性:

    1. self.customer 指向了 annCustomer 实例)。
    2. self.cart 是存储了商品的列表。
    3. self.promotion 存储了函数 fidelity_promo
    内存示意:
    order ──> [Customer, Cart, Promotion]
               /         /         \
            ann        cart      fidelity_promo 
    

第二步:调用 order.due()

  • order.due() 方法中会调用 self.promotion(self)

    • 此时 self.promotion 是一个已绑定的 fidelity_promo 函数。

    • selforder 实例本身。

    • 内存内部的表现:

      self.promotion     --> fidelity_promo
      fidelity_promo     --> 函数对象(代码 + 参数规则 + 指向作用域)
      self (传递给函数) --> order 实例
      

第三步:执行 fidelity_promo(self)

  • fidelity_promo(self) 调用中:

    1. self 参数被绑定到 order 实例。
    2. 函数访问了传入的 order 数据,执行了 order.total() 和相关的逻辑(忠诚度积分检查等)。
    3. 函数返回了处理后的折扣值。
  • 当函数返回时,折扣被用于计算总应付金额。

    内存更新:
    order ───> fidelity_promo(order)  返回 Decimal('2.10')
    

selfOrder 的关系

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)。

我们的任务是:

  1. 收集所有可能的折扣策略
  2. 计算给定订单的最佳折扣(即最大折扣)

针对这个需求,我们将实现一个函数 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>
运行逻辑分析
  1. promos 列表
    promos 是一个函数列表,存储了所有已经实现的折扣策略函数,如 fidelity_promo, bulk_item_promo, 和 large_order_promo

    • 这种方式展示了函数作为“一等对象”的强大特性:函数可以像变量一样存储在列表中,稍后动态调用。
  2. best_promo函数

    • 对于给定订单 order,函数逐一调用 promos 列表中的每个折扣策略函数 promo
    • 使用 max 函数获取所有折扣值中的最大值。
  3. 代码优劣分析
    优点:

    • 灵活性:折扣策略是独立函数,彼此隔离,功能清晰。
    • 易扩展性:可以轻松添加或替换折扣策略。

    潜在问题:

    • 不确定性: 如果忘记将新折扣策略添加到 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)

代码详解

  1. globals() 解析模块全局命名空间:

    • globals() 返回当前模块的全局定义表(即变量、函数等),是一个字典。
    • 每个全局符号的名字作为键,对应的对象作为值。
    • 示例中,我们通过字典解析 globals().items() 遍历所有全局定义。
  2. 筛选符合条件的函数对象:

    • 筛选名称以 _promo 结尾的对象(符合策略命名约定)。
    • 排除 best_promo 函数本身,防止循环调用造成无限递归错误。
  3. 自动化函数集合:

    • 提前显式导入所有策略函数。
    • 使用列表推导式,根据命名约定动态收集这些函数,加入到 promos 列表。
  4. 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)

代码详解

  1. inspect.getmembers() 的作用:

    • getmembers 获取一个对象的所有属性及其对应值。
    • 参数:
      • 第一个参数:要 introspect 的对象(这里是 promotions 模块)。
      • 第二个参数:可选谓词函数,用于筛选满足条件的属性(这里是 inspect.isfunction,表示只关心函数)。

    通过这种方式,我们可以从指定模块 promotions 中精准找到所有属于自定义策略类的函数。

  2. 收集函数的特点:

    • 相比 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] 的理解:

  1. 类型别名用赋值操作:
    使用赋值语法 Name = SomeType 创建一个类型别名,这是一种简便方式,用来省略重复书写复杂类型提示的麻烦。具体示例:

    Promotion = Callable[[Order], Decimal]  # 创建类型别名
    
  2. 类型标注用冒号 :
    使用冒号为变量、函数参数或返回值做类型声明。例如:

    def apply_promotion(order: Order, func: Promotion) -> Decimal:
        ...
    
  3. 两者结合使用:我们可以先用赋值语法定义一个类型别名,然后利用 : 来标注类型,使代码既简洁清晰又方便维护。

promotion 函数的作用:

  • 核心功能:将被装饰的促销策略函数自动加入到 promos 列表中,无需手动维护。
  • 设计思路:返回的 promo 函数不被改变,装饰器仅增加一个附加效果(注册到列表)。
  • 优点:避免了遗漏新增策略的风险,引入了自动化注册机制。
  1. 装饰器的本质:

    • 装饰器接受一个函数作为参数,并返回一个函数(可以是修改过的版本,也可以不)。
    • 在这个例子中,promotion 是一个装饰器,它接收一个函数 promo,将其注册到 promos 列表中后,直接返回这个函数本身。
  2. 装饰器的目的:

    • 你可以把装饰器看作“包装”函数,可以在函数的定义时动态增加一些功能。
    • 这里的装饰器 promotion 的功能是 动态注册促销策略函数 (收集所有被装饰的函数),它直接将函数添加到全局的 promos 列表中,非常便于管理。
  3. 简化形式:

    • 这个装饰器并没有嵌套函数,因为它没有对函数的行为做任何修改——它只是将函数注册进列表。
    • 因此,它可以简化为直接返回的形式:return promo,不需要定义内部 wrapper 函数。

解释具体的运行流程:

  1. 当你使用 @promotion 装饰 fidelity 函数时:

    • 函数 fidelity 会被传递给装饰器 promotion
    • fidelity 会被添加到 promos 列表中。
    • 装饰器返回“未修改的原始函数”,所以 fidelity 原本的行为没有任何改变。
  2. 最终,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)操作。

命令模式中的主要角色

  1. Invoker(调用者)
    发起命令请求的对象。在例子中是 Menu 的实例。

  2. Command(命令接口)
    定义了命令的接口方法(如 execute())。通过具体的命令类来实现该接口。

  3. Concrete Commands(具体命令类)

    • OpenCommand, PasteCommand 等。
    • 每个具体命令类都绑定一个接收者,执行特定操作。
  4. Receiver(接收者)
    实现实际执行逻辑的对象,例如 DocumentApplication

  5. Client(客户代码)
    将具体命令对象与调用者关联。

在这里插入图片描述

二、命令模式的具体实现

  1. 示例 UML(见上图)
    说明

    • Menu 是调用者,负责调用绑定的命令。
    • 每个命令都有自己的接收者。
    • MacroCommand 可以存储多个命令,并按照顺序依次调用它们。
  2. 基本代码实现:
    传统命令模式

    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'

层层嵌套,无穷无尽。