10. 第10章 使用一等函数实现设计模式

10. 章使用一等函数实现设计模式

符合模式并不表示做得对.
                                                   --Ralph Johnson经典著作<<设计模式>>的作者之一①
                                                   
(1: 出自20141115日Ralph Johnson在圣保罗大学IME/CCSL所做的题为
'Root Cause Analysis ofSome Faults in Design Patterns'的演讲.)
在软件工程中, 设计模式指解决常见设计问题的一般性方案.
阅读本章无须事先知道任何设计模式. 我会解释示例中用到的设计模式.
编程设计模式的使用通过Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides
('四人组')合著的<<设计模式>>一书普及开来.
该书涵盖23个模式, 通过C++代码定义的类编排. 不过, 这些模式也可用于其他面向对象语言.

虽然设计模式与语言无关, 但这并不意味着每一个模式都能在每一门语言中使用.
例如, 读到第17章你会发现, 在Python中效仿迭代器模式毫无意义, 因为该模式深植语言之中, 通过生成器即可使用.
在Python中, 迭代器模式无须'劳烦', 实现代码也比典型方案少.

<<设计模式>>的作者在引言中承认, 所用的语言决定了哪些模式可用.

程序设计语言的选择非常重要, 它将影响人们理解问题的出发点.
我们的设计模式采用了Smalltalk和C++层的语言特性, 
这个选择实际上决定了哪些机制可以方便地实现, 哪些则不能.
如果采用过程式语言, 那么可能就要包括诸如'继承' '封装''多态'的设计模式.
相应地, 一些特殊的面向对象语言可以直接支持我们的某些模式.
例如, CLOS支持多方法概念, 这就减少了访问者等模式的必要性.

1996, Peter Norvig在题为'Design Patterns in Dynamic Languages'的演讲中指出,
对于<<设计模式>>一书提出的23个模式, 16个在动态语言中'要么不见了, 要么简化了'(9张幻灯片).
他讨论的是Lisp语言和Dylan语言, 不过很多相关的动态特性在Python中也能找到.
具体而言, Norvig建议在有一等函数的语言中重新审视'策略' '命令' '模板方法''访问者'等经典模式.

本章的目标是展示在某些情况下, 函数可以起到类的作用, 而且写出的代码可读性更高且更简洁.
我们会把函数作为对象使用, 重构策略模式, 从而减少大量样板代码.
还会讨论一种类似的方案, 以简化命令模式.

10.1 本章新增内容
我把本章移到了第二部分末尾, 这样就可以在10.3节应用一个注册装饰器, 以及在示例中使用类型提示了.
本章出现的大多数类型提示并不复杂, 却能提升代码可读性.
10.2案例分析: 重构策略模式
如果合理利用作为一等对象的函数, 则某些设计模式可以简化, 而策略模式就是一个很好的例子.
本节接下来的内容将说明策略模式的作用, 并使用<<设计模式>>一书中所述的'经典'结构来实现它.
如果你熟悉这个经典模式, 则可以跳到10.2.2, 了解如何使用函数重构代码, 极大地减少代码行数.
10.2.1 经典的策略模式
10-1中的UML类图指出了策略模式对类的编排.

strategy

10-1: 使用策略设计模式处理订单折扣的UML类图.
<<设计模式>>一书对策略模式的概述如下.
    定义一系列算法, 把它们一一封装起来, 并且使它们可以相互替换.
    本模式使得算法可以独立于使用它的客户而变化.
    
在电商领域应用策略模式的一个典型例子是根据客户的属性或订单中的商品计算折扣.
假如某个网店制定了以下折扣规则.
 1000或以上积分的顾客, 每个订单享5%折扣.
 同一订单中, 单个商品的数量达到20个或以上, 10%折扣.
 订单中不同商品的数量达到10个或以上, 7%折扣.
为简单起见, 假定一个订单一次只能享用一个折扣.

策略模式的UML类图见图10-1, 其中涉及下列内容.
上下文
  提供一个服务, 把一些计算委托给实现不同算法的可互换组件.
  在这个电商示例中, 上下文是order, 它根据不同的算法计算促销折扣.
  
策略
  实现不同算法的组件共同的接口.
  在这个示例中, 名为Promotion的抽象类扮演这个角色.

具体策略
  策略的具体子类.
  FidelityPromo, BulkPromo和LargeOrderPromo是这里实现的3个具体策略.
# 示例10-1实现Order类, 支持插入式折扣策略.
from abc import ABC, abstractmethod
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]  # 购物车 --> 订单条目对象组成的序列.
    # 促销活动 --> 促销活动类型 或者 None类型, 默认值为None.
    promotion: Optional['Promotion'] = None  

    # 计算订单总价 -> 返回高精度浮点型.
    def total(self) -> Decimal:
        # 遍历订单中每个的条目的价格.
        totals = (item.total() for item in self.cart)
        # 统计总价, 统计之前先与Decimal(0)相加.
        return sum(totals, start=Decimal(0))

    # 待支付金额 -> 返回高精度浮点型.
    def due(self) -> Decimal:
        # 判断是否有促销活动.
        if self.promotion is None:
            # 折扣价格为0元.
            discount = Decimal(0)
        # 有促销活动.
        else:
            # 减免的金额为x元.
            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):  # 策略:抽象基类

    # 继承这个类的子类, 必须实现discount(折扣方法).
    @abstractmethod
    def discount(self, order: Order) -> Decimal:  # 接受订单类, 返回高精度浮点型.
        """返回折扣金额(正值)"""


# 第一个具体策略.
class FidelityPromo(Promotion):
    """为积分为1000或以上的顾客提供5%折扣"""

    def discount(self, order: Order) -> Decimal:
        # 折扣率为5%.
        rate = Decimal('0.05')
        # 判断客户的积分是否大于或等于1000.
        if order.customer.fidelity >= 1000:
            # 返回需要减免的金额.
            return order.total() * rate
        return Decimal(0)


# 第二个具体策略.
class BulkItemPromo(Promotion):
    """单个商品的数量为20个或以上时提供10 % 折扣"""

    def discount(self, order: Order) -> Decimal:
        # 折扣金额初始化为0.
        discount = Decimal(0)
        # 遍历购物车, 判断单个商品的的数量是否大于或等于20.
        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}
        # 不同商品的数量到达10个或以上提供7%的折扣, 否则没有.
        if len(distinct_items) >= 10:
            return order.total() * Decimal('0.07')
        return Decimal(0)

注意, 示例10-1把Promotion定义为了抽象基类, 
这么做是为了使用@abstractmethod装饰器, 明确表明所用的模式.

示例10-2是一些doctest, 用于在某个实现了上述规则的模块中演示和验证相关操作.
# 示例10-2 使用不同促销折扣的Order 类示例

# 两位顾客: joe的积分是0, ann的积分是1100.
>>> joe = Customer('John Doe', 0)
>>> ann = Customer('Ann Smith', 1100)

# 购物车中有3个商品. (商品名称, 数量, 单价)
>>> cart = (LineItem('banana', 4, Decimal('.5')),
...         LineItem('apple', 10, Decimal('1.5')),
...         LineItem('watermelon', 5, Decimal(5)))

# FidelityPromo 未给joe提供折扣.
>>> Order(joe, cart, FidelityPromo())
<Order total:42.00 due: 42.00>

# ann得到了5%折扣, 因为她的积分超过1000.
>>> Order(ann, cart, FidelityPromo())
<Order total: 42.00 due: 39.90>

# banana_cart中有30把香蕉和10个苹果.
>>> banana_cart = (LineItem('banana',30, Decimal('.5')), 
...                LineItem('apple',10, Decimal('1.5')))

# BulkItemPromo 为joe购买的香蕉优惠了1.5美元.
>>> Order(joe, banana_cart, BulkItemPromo()) 
<Order total: 30.00 due: 28.50>

# long_order中有10个不同的商品, 每个商品的价格为1美元.
>>> long_cart = tuple(LineItem(str(sku), 1, Decimal(1))
...                   for sku in range(10))

# LargerOrderPromo 为joe的整个订单(有10个不同的商品)提供7%折扣.
>>> Order(joe, long_cart, LargeOrderPromo())
<Order total: 10.00 due:9.30>

# LargerOrderPromo 为joe的订单(三个商品)没有提供折扣.
>>> Order(joe, cart, LargeOrderPromo())
<Order total: 42.00 due: 42.00>

示例10-1完全可用, 但是在Python中, 如果把函数当作对象使用, 则实现同样的功能所需的代码更少.
详见10.2.2.
10.2.2 使用函数实现策略模式
在示例10-1, 每个具体策略都是一个类, 而且都只定义了一个方法, 即discount.
此外, 策略实例没有状态(没有实例属性). 你可能会说, 它们看起来像是普通函数--的确如此.
示例10-3是对示例10-1的重构, 把具体策略换成了简单的函数, 而且去掉了抽象类Promo.
Order类也要修改, 但改动不大. 

(2: 由于Mypy存在bug, 因此不得不使用@dataclass重新实现Order.
你可以忽略这个细节, 因为也可以像示例10-1那样继承NanedTuple.
如果Order继承NamedTuple, 则Mypy 0.910在检查promotion的类型提示时将崩溃.
我试过在那一行添加#type ignore, 不过 Mypy依旧崩溃.
如果使用@dataclass构建Order, 那么Mypy就能正确处理相同的类型提示了.
截至2021719, 9397号工单还没有解决. 但愿等你读到这里时已经修正了.)
# 示例10-3 Order类和使用函数实现的折扣策略.
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]  # 购物车 --> 订单条目对象组成的序列.
    # 促销活动 --> 类型为可调用对象 或 None, 默认值为None.
    # 这个类型提示的意思是, promotion既可以是None, 
    # 也可以是接收一个Order参数并返回一个Decimal值的可调用对象.
    promotion: Optional[Callable[['Order'], Decimal]] = None

    # 计算订单总价 -> 返回高精度浮点型.
    def total(self) -> Decimal:
        # 遍历订单中每个的条目的价格.
        totals = (item.total() for item in self.cart)
        # 统计总价, 统计之前先与Decimal(0)相加.
        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):
        # 在打印对象时, 调用total()方法与due()方法.
        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()
    return discount * Decimal('0.1')


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)

*---------------------------------------------------------------------------------------------*
为什么写成self.promotion(self)?
在Order类中, promotion不是方法, 而是一个实例属性, 只不过它的值是可调用对象.
因此, 作为表达式的第一部分, self.promotion的作用是获取可调用对象.
为了调用得到的可调用对象, 必须提供一个Order实例, 即表达式中的self.
因此, 表达式中出现了两个self.

23.4节将解释自动为实例绑定方法的机制.
这个机制不适用于promotion, 因为promotion不是方法.
*---------------------------------------------------------------------------------------------*
示例10-3中的代码比示例10-1简短.
不仅如此, 新的Order类使用起来更简单, 如示例10-4中的doctest所示.
# 示例10-4 以函数实现促销折扣的Order类使用示例

# 与示例10-1一样的测试固件.
>>> 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))]

# 为了把折扣策略应用到Order实例上, 只需把促销函数作为参数传入即可.
>>> Order(joe, cart, fidelity_promo)
<Order total:42.00 due: 42.00>

>>> 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'))]

# 这个测试和下一个测试使用了不同的促销函数.
>>> 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)]

>>> Order(joe, long_cart, large_order_promo)
<Order total: 10.00 due: 9.30>

>>> Order(joe, cart, large_order_promo)
<Order total: 42.00 due: 42.00>

注意示例10-4中的标号: 没必要在新建订单时实例化新的促销对象, 函数拿来即用.

值得注意的是, <<设计模式>>一书指出: '策略对象通常是很好的享元(flyweight).'
该书的另一部分对'享元'下了定义: '享元是可共享的对象, 可以同时在多个上下文中使用.'
共享是推荐的做法, 这样在每个新的上下文(这里是Order实例)
中使用相同的策略时则不必不断新建具体策略对象, 从而减少消耗.
因此, 为了避免策略模式的一个缺点(运行时消耗),
<<设计模式>>一书建议再使用另一个模式. 但是这样做, 代码行数和维护成本会不断攀升.

在复杂的情况下, 当需要用具体策略维护内部状态时, 可能要把策略和享元模式结合起来.
但是, 具体策略一般没有内部状态, 只负责处理上下文中的数据.
此时, 一定要使用普通函数, 而不是编写只有一个方法的类, 再去实现另一个类声明的单函数接口.
函数比用户定义的类的实例轻量, 而且无须使用享元模式, 因为各个策略函数在Python加载模块时只创建一次.
普通函数也是'可共享的对象, 可以同时在多个上下文中使用'.

至此, 我们使用函数实现了策略模式, 由此也出现了其他可能性.
假设我们想创建一个'元策略', 为Order选择最佳折扣.
接下来的几节会接着重构, 利用函数和模块都是对象这一特点, 使用不同的方式实现这个需求.
10.2.3 选择最佳策略的简单方式
下面继续使用示例10-4中的顾客和购物车, 在此基础上添加3个测试, 如示例10-5所示.
# 示例10-5 best_promo函数计算所有折扣, 返回幅度最大的那一

# best_promo 为顾客 joe 选择larger_order_promo.
>>> Order(joe, long_cart, best_promo)
<Order total: 10.00 due: 9.30>

# 订购大量香蕉时, joe使用bulk_item_promo提供的折扣.
>>> Order(joe, banana_cart, best_promo)
<Order total: 30.00 due: 28.50>

# 在一个简单的购物车中, best_promo 为忠实顾客ann提供fidelity_promo优惠的折扣.
>>> Order(ann, cart, best_promo)
<Order total:42.00 due: 39.90>

best_promo 函数的实现特别简单, 如示例10-6所示.
# 示例10-6 best_promo迭代一个函数列表, 找出折扣幅度最大的那一个

# promos列出以函数实现的各个策略.
promos = [fidelity_promo, bulk_item_promo, large_order_promo]

# 与其他几个*_promo函数一样, best_promo函数的参数是一个Oder实例.
def best_promo(order: Order) -> Decimal:
    """选择可用的最佳折扣"""
    # 使用生成器表达式把order传给promos列表中的各个函数, 返回折扣幅度最大的那个函数.
    return max(promo(order) for promo in promos)

# max(i for i in range(10)) 和 max((i for i in range(10))) 是等价的表达式, 前者是缩写.

示例10-6简单明了, promos是函数列表.
习惯函数是一等对象后, 自然而然就会构建那种数据结构来存储函数.

虽然示例10-6可行, 而且易于理解, 但是存在一些重复, 可能导致不易察觉的bug:
如果想添加新的促销策略, 那么不仅要定义相应的函数, 还要记得把它添加到promos列表中;
否则, 只能把新促销函数作为参数显式传给Order, 因为best_promo不知道新函数的存在.
这个问题的解决方案有好几种, 请继续往下读.
10.2.4 找出一个模块中的全部策略
在Python中, 模块也是一等对象, 而且标准库提供了几个处理模块的函数.
Python文档对内置函数globals的描述如下.

globals()
  返回一个字典, 表示当前的全局符号表.
  这个符号表始终针对当前模块(对函数或方法来说, 是指定义它们的模块, 而不是调用它们的模块).

示例10-7使用globals函数帮助best_promo自动找到了其他可用的*_promo函数, 过程有点儿曲折.
# 示例10-7 内省模块的全局命名空间, 构建promos列表
from decimal import Decimal

from strategy import Order
# 导入促销函数, 以便其在全局命名空间中可用. ③
from strategy import (
    fidelity_promo, bulk_item_promo, large_order_promo)

# 迭代globals()返回的字典中的各项.
promos = [promo for name, promo in globals().items()
          # 只选择以_promo结尾的值.
          if name.endswith('_promo') and
          # 过滤掉best_promo自身, 防止调用best_promo时出现无限递归.
          name != 'best_promo'
          ]


# best_promo没有变化.
def best_promo(order: Order) -> Decimal:
    """选择可用的最佳折扣"""
    return max(promo(order) for promo in promos)

2023-06-11_00001

3: flake8和VS Code都会发出警告, 指出虽然导入了这些名称, 但是并没有使用它们.
按照定义, 静态分析工具不能理解Python的动态本性.
如果凡事都听这些工具的, 那么结果必然是以Python句法写出类似Java那种冗长晦涩, 质量低下的代码.
收集所有可用促销的另一种方法是, 在一个单独的模块中保存所有的策略函数(best_promo除外).

在示例10-8, 最大的变化是内省名为promotions的独立模块, 构建策略函数列表.
注意, 示例10-8要导入promotions模块, 以及提供高阶内省函数的inspect模块.
# 示例10-8内省单独的promotions模块, 构建promos列表
from decimal import Decimal
import inspect
from strategy import Order
# 这个模块没有实现(这里只是假定).
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)

# promotions.py promotions模块只能有构建策略函
from (有Order的模块) import Order
from decimal import Decimal



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()
    return discount * Decimal('0.1')


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)

inspect.getmembers函数用于获取对象(这里是promotions 模块)的属性,
第二个参数是可选的判断条件(一个布尔值函数).
这里使用的判断条件是inspect.isfunction, 只获取模块中的函数.

不管怎么命名策略函数, 示例10-8都行之有效, 唯一的要求是, promotions模块只能包含计算订单折扣的函数.
当然, 这是对代码的隐性假设. 如果有人在promotions模块中使用不同的签名来定义函数, 
那么在best_promo函数尝试将其应用到订单上时会出错.

可以添加更为严格的测试, 审查传给实例的参数, 进一步筛选函数.
示例10-8的目的不是提供完善的方案, 而是强调模块内省的一种用途.
动态收集促销折扣函数的一种更为显式的方案是使用简单的装饰器. 详见10.3.
10.3 使用装饰器改进策略模式
回顾一下, 示例10-6的主要问题是, 定义中出现了函数的名称, 
best_promo用来判断哪个折扣幅度最大的promos列表中也有函数的名称.
这种重复是个问题, 因为新增策略函数后可能会忘记把它添加到promos列表中, 
导致best_prono悄无声息忽略新策略, 为系统引入不易察觉的bug.
示例10-9使用9.4节介绍的技术解决了这个问题.
# 示例10-9 promos列表中的值使用 promotion装饰器填充
from collections.abc import Callable
from strategy import Order  # 前面自定义的订单类型
from decimal import Decimal

Promotion = Callable[[Order], Decimal]
# promos列表位于模块全局命名空间中, 起初是空的.
promos: list[Promotion] = []


# Promotion是注册装饰器, 在把promo函数添加到promos列表中之后, 它会原封不动地返回promo函数.
def promotion(promo: Promotion) -> Promotion:
    promos.append(promo)
    return promo


def best_promo(order: Order) -> Decimal:
    """选择可用的最佳折扣"""
    # 无须修改best_promos, 因为它依赖于promos列表.
    return max(promo(order) for promo in promos)


# 被@promotion装饰的函数都会添加到promos列表中.
@promotion
def fidelity(order: Order) -> Decimal:
    """为积分为1000或以上的顾客提供5%折扣"""
    if order.customer.fidelity >= 1000:
        return order.total() * Decimal('0.05')
    return Decimal(0)


@promotion
def bulk_item(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


@promotion
def large_order(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)

与前几种方案相比, 这种方案有以下几个优点.
 促销策略函数无须使用特殊的名称(不用以_promo结尾).
  @promotion 装饰器突出了被装饰的函数的作用, 还便于临时禁用某个促销策略:
   把装饰器注释掉即可.
 促销折扣策略可以在其他模块中定义, 在系统中的任何地方都行, 只要使用了@promotion装饰器.
10.4节将讨论命令模式.
这个设计模式也常使用单方法类实现, 同样也可以换成普通函数.
10.4 命令模式
命令设计模式也可以通过把函数作为参数传递而简化. 这一模式对类的编排如图10-2所示.

图10-2

10-2: 菜单驱动的文本编辑器的UML类图, 使用命令设计模式实现. 
各个命令可以有不同的接收者, 即实现操作的对象.
对PasteCommand来说, 接收者是Document; 对OpenCommand来说, 接收者是应用程序.
命令模式的目的是解耦调用操作的对象(调用者)和提供实现的对象.
(接收者)<<设计模式>>一书所举的示例中, 调用者是图形应用程序中的菜单项, 
接收者是被编辑的文档或应用程序自身.

这个模式的做法是, 在二者之间放一个Command 对象, 让它实现只有一个方法(execute)的接口,
调用接收者中的方法执行所需的操作.
这样, 调用者无须了解接收者的接口, 而且不同的接收者可以适应不同的Command子类.
调用者有一个具体的命令, 通过调用execute方法执行.
注意, 10-2的MacroCommand可能会保存一系列命令, 它的execute()方法会在各个命令上调用相同的方法.

Gamma等人说过: '命令模式是回调机制的面向对象替代品.'
问题是, 我们需要回调机制的面向对象替代品吗? 有时确实需要, 但并非始终需要.

可以不为调用者提供Command实例, 而是给它一个函数.
此时, 调用者不调用command.execute(), 而是直接调用command().
MacroCommand可以通过定义了call_方法的类实现.
这样, MacroCommand的实例就是可调用对象, 各自维护着一个函数列表, 供以后调用.
MacroCommand 的实现如示例10-10所示.
# 示例10-10 MacroCommand的各个实例都在内部存储着命令列表

class MacroCommand:
    """一个执行一组命令的命令"""
    def __init__(self, commands):
        # 根据commands参数构建一个列表, 这样能确保参数是可迭代对象, 
        # 还能在各个MacroCommand 实例中保存各个命令引用的副本.
        self.commands = list(commands) 
        
    def __call__(self):
        # 调用MacroCommand实例时, self.commands中的各个命令依序执行.
        for command in self.commands:
            command()
            
复杂的命令模式(例如支持撤销操作)可能需要的不仅仅是简单的回调函数. 即便如此.
也可以考虑使用Python提供的几个替代品.
 像示例10-10中MacroCommand那样的可调用实例, 可以保存任何所需的状态,
  除了__call__还可以提供其他方法.
 可以使用闭包在调用之间保存函数的内部状态.
使用一等函数对命令模式的重新审视到此结束. 
站在一定高度上看, 这里采用的方式与策略模式所用的方式类似:
把实现单方法接口的类的实例替换成可调用对象.
毕竟, 每个Python可调用对象都实现了单方法接口, 这个方法就是__call__.

10.5 本章小结
经典著作<<设计模式>>出版几年后, Peter Norvig指出: 
'在Lisp或Dylan中, 23个设计模式中有16个的实现方式比在C++中更简单,
而且能保持同等质量, 至少各个模式的某些用途如此.'
(Norvig的“Design Patterns in Dynamic Languages'演讲, 9张幻灯片.)
Python有些动态特性与Lisp和Dylan一样, 尤其是本章着重讨论的一等函数.

本章开头引用的那甸话是Ralph Johnson在纪念<<设计模式>>英文原书出版20周年活动上所说的,
他指出了该书的缺点之一: '过多强调设计模式的结果, 而没有细说过程.'
本章从策略模式开始, 使用一等函数简化了实现方式.

很多情况下, 在Python中使用函数或可调用对象实现回调更自然, 
这比模仿Gamma, Helm, Johnson  Vlissides在<<设计模式>>一书中所述的策略或命令模式要好.
④本章对策略模式的重构和对命令模式的讨论是为了通过示例说明一种更为常见的做法:
有时, 设计模式或API要求组件实现单方法接口, 而该方法有一个很宽泛的名称, 
例如'execute' 'run''do_it'.
在Python中, 这些模式或API通常可以使用作为一等对象的函数实现, 从而减少样板代码.

4: 2014 1115 Johnson 在IME-USP 所做的题为
'Root Cause Analysis of Some Faults in Design Patterns'的演讲.

10.6延伸阅读
<<Python Cookbook(3) 中文版>> 8.21节使用优雅的方式实现了访问者模式,
其中的NodeVisitor 类把方法当作一等对象处理.
在设计模式方面, Python程序员的阅读选择没有其他语言多.

据我所知, Learning Python Design Patterns(Gennadiy Zlobin著)
是目前唯一一本专门讲解Python 设计模式的书.
不过, 该书特别薄(100), 只涵盖了23个设计模式中的8. 
<<Python 高级编程>>是目前市面上值得一读的Python中级书, 
该书最后一章从Python程序员的视角介绍了几个经典模式.

Alex Martelli做过几次关于Python设计模式的演讲.
他在EuroPython 2011上的演讲有视频, 他的个人网站中有一些幻灯片.
这些年, 我找到过不同的幻灯片和视频, 长短不一, 
因此你要仔细搜索他的名字和'Python Design Patterns'这些词.
有家出版商告诉我, Martelli正在写一本关于这个话题的书. 出版后我肯定会拜读.

Java相关的设计模式书很多, 其中我最喜欢的是<<Head First设计模式(第二版)>>.
该书讲解了23个经典模式中的16.
如果喜欢Head First系列丛书的古怪风格, 而且想了解这个主题, 你会喜欢该书的.
该书围绕Java展开, 不过第二版做了更新, 涵盖了Java中的一等函数, 因此部分示例与Python版本接近.

如果想换个新鲜的角度, 从支持鸭子类型和一等函数的动态语言入手, 
那么<<Ruby设计模式>>一书中有很多见解也适用于Python.
虽然Python和Ruby在句法上有很多区别, 但是二者在语义方面很接近, 比Java或C++接近.
'Design Patterns in Dynamic Languages'演讲中, 
Peter Norvig展示了如何使用一等函数和其他动态特性简化几个经典的设计模式, 或者彻底摒除设计模式.

光看<<设计模式>>一书的'引言'就能让你赚回书钱.
在引言中, 几位作者对23个设计模式一一编目, 涵盖非常重要和很少使用的模式.
人们经常从该书引言中引用两个设计原则, 
'对接口编程, 而不是对实现编程''优先使用对象组合, 而不是类继承'.

模式在设计中的应用源自<<建筑模式语言>>一书.
Alexander的想法是创建一个标准的表现形式, 让团队在设计建筑时共享共同的设计决策.
M.J. Dominus "‘Design Patterns Aren't"演讲中认为,
Alexander 对模式最初的愿景更深远, 更人性化, 在软件工程中的应用也应如此.
*---------------------------------------------杂谈--------------------------------------------*
Python拥有一等函数和一等类型, Norvig声称, 
这些功能对23个模式中的10个有影响('Design Patterns in Dynamic Languages', 10张幻灯片).
如第9章所述, Python也有泛型函数(参见9.9.3).
这与CLOS中的多方法类似, Gamma等人建议使用多方法以一种简单的方式实现经典的访问者(Visitor)模式.
Norvig却说, 多方法能简化生成器(Builder)模式(10张幻灯片).
可见, 设计模式与语言功能无法精确对应.

世界各地的课堂经常使用Java示例讲解设计模式.
我不止一次听学生说过, 他们以为设计模式在任何语言中都有用.
<<设计模式>>一书中提出的23'经典'模式, 大多使用C++代码说明, 少量使用Smalltalk.
事实证明, 这些设计模式能很好地应用到Java上.
然而, 这并不意味着所有模式都能一成不变地在任何语言中运用.
该书的作者在开头就明确表明, '一些特殊的面向对象语言可以直接支持我们的某些模式'(完整的引用见本章开头).

与Java, C++或Ruby相比, Python设计模式方面的图书都很薄.
延伸阅读中提到的Learning Python Design Patterns (Gennadiy Zlobin著)201311月才出版.
<<Ruby设计模式>>2007年就出版了, 英文版有384, 比Zlobin的那本书多出284.
如今, Python在学术界越来越流行, 希望以后会有更多以这门语言讲解设计模式的图书.
此外, Java8引入了方法引用和匿名函数, 这些广受期盼的功能有可能为Java催生新的模式实现方式——要知道, 
语言会进化, 因此运用经典设计模式的方式必定要随之进化.

'夺命连环call':
为本书做最后的润色时, 技术审校Leonardo Rochael提出了一个有趣的问题:
如果函数有__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'

子子孙孙无穷匮也.
*--------------------------------------------------------------------------------------------*
  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值