在软件工程中,设计模式指解决常见设计问题的一般性方案。编程设计模式的使用通过 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides(四人组)合著的《设计模式》一书普及开来。虽然设计模式与语言无关,但这并不意味着每一个模式都能在每一门语言中使用,例如迭代器模式就深植 Python 语言之中。
本章的目标是展示在某些情况下,函数可以起到类的作用,而且写出的代码可读性更高且更简洁。
重构策略模式
如果合理利用作为一等对象的函数,则某些设计模式可以简化,而策略模式就是一个很好的例子。如下 UML 类图指出了策略模式对类的编排:
上下文:提供一个服务,把一些计算委托给实现不同算法的可互换组件,即这里的 Order 类
策略:实现不同算法的组件共同的接口,即这里的 Promotion 类
策略的具体子类:FidelityPromo、BulkPromo 和 LargeOrderPromo 是这里实现的 3 个具体策略
具体策略由上下文类的客户选择。如下是一个实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 from abc import ABC, abstractmethodfrom collections.abc import Sequence from decimal import Decimalfrom 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 ): return f'<Order total: {self.total():.2 f} due: {self.due():.2 f} >' class Promotion (ABC ): @abstractmethod def discount (self, order: Order ) -> Decimal: """返回折扣金额(正值)""" 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 )
把 Promotion 定义为了抽象基类,这么做是为了使用 @abstractmethod 装饰器,明确表明所用的模式
虽然这个例子完全可用,但是在 Python 中,如果把函数当作对象使用,则实现同样的功能所需的代码更少。
使用函数实现策略模式
上面的例子中,每个具体策略都是一个类,而且都只定义了一个方法,即 discount。此外,策略实例没有状态(没有实例属性),因此可以把具体策略换成了简单的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 from collections.abc import Sequence from dataclasses import dataclassfrom decimal import Decimalfrom typing import Optional , Callable , NamedTupleclass 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 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():.2 f} due: {self.due():.2 f} >' 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 )
promotion 的类型提示,含义是 promotion 既可以是 None,也可以是接收一个 Order 参数并返回一个 Decimal 值的可调用对象
为了把折扣策略应用到 Order 实例上,只需把促销函数作为参数传入即可
值得注意的是,《设计模式》一书指出:策略对象通常是很好的享元(flyweight)。该书的另一部分对 享元 下了定义:享元是可共享的对象,可以同时在多个上下文中使用。
共享是推荐的做法,这样在每个新的上下文(这里是 Order 实例)中使用相同的策略时则不必不断新建具体策略对象,从而减少消耗。
在复杂的情况下,当需要用具体策略维护内部状态时,可能要把策略和享元模式结合起来
但是,具体策略一般没有内部状态,只负责处理上下文中的数据。此时,一定要使用普通函数,而不是编写只有一个方法的类,再去实现另一个类声明的单函数接口。而且普通函数本身就是可共享的对象,无需额外的享元机制
至此,我们使用函数实现了策略模式,由此也出现了其他可能性。假设我们想创建一个 元策略,为 Order 选择最佳折扣。
选择最佳策略的简单方式
如下实现了一个简单的最佳策略选择:
1 2 3 4 5 6 promos = [fidelity_promo, bulk_item_promo, large_order_promo] def best_promo (order: Order ) -> Decimal: """选择可用的最佳折扣""" return max (promo(order) for promo in promos)
promos 是函数列表。习惯函数是一等对象后,自然而然就会构建那种数据结构来存储函数
虽然这个代码简单,但是如果想添加新的促销策略,那么不仅要定义相应的函数,还要记得把它添加到 promos 列表中。为了解决这个问题,有几种解决方案。
找出一个模块中的全部策略
在 Python 中,模块也是一等对象,而且标准库提供了几个处理模块的函数。globals() 返回一个字典,表示当前的全局符号表。这个符号表始终针对当前模块(对函数或方法来说,是指定义它们的模块,而不是调用它们的模块)。
如下代码内省模块的全局命名空间,构建 promos 列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 from decimal import Decimalfrom strategy import Orderfrom 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)
这种方式还是要记得 import 对应的 xxx_promo 函数。另一种解决方案在一个单独的模块中保存所有的策略函数(best_promo除外):
1 2 3 4 5 6 7 8 9 10 11 12 from decimal import Decimalimport inspectfrom strategy import Orderimport promotionspromos = [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 函数用于获取对象(这里是 promotions 模块)的属性,第二个参数是可选的判断条件,这里使用 inspect.isfunction 函数判断属性是否为函数
当然这种方案并不完善,如果有人在 promotions 模块中使用不同的签名来定义函数,那么在 best_promo 函数尝试将其应用到订单上时会出错。
使用装饰器改进策略模式
动态收集促销折扣函数的一种更为显式的方案是使用简单的装饰器。定义中出现了函数的名称,best_promo 用来判断哪个折扣幅度最大的 promos 列表中也有函数的名称,这种重复是个问题,因为新增策略函数后可能会忘记把它添加到 promos 列表中,导致不易察觉的 bug。
如下代码使用装饰器解决这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 Promotion = Callable [[Order], Decimal] promos: list [Promotion] = [] def promotion (promo: Promotion ) -> Promotion: promos.append(promo) return promo def best_promo (order: Order ) -> Decimal: """选择可用的最佳折扣""" return max (promo(order) for promo in 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 )
Promotion 是注册装饰器,在把 promo 函数添加到 promos 列表中之后,它会原封不动地返回 promo 函数
使用这种方案有多个优点:
促销策略函数无须使用特殊的名称
@promotion 装饰器突出了被装饰的函数的作用,还便于临时禁用某个促销策略:把装饰器注释掉即可
促销折扣策略可以在其他模块中定义,在系统中的任何地方都行,只要使用了 @promotion 装饰器
命令模式
命令设计模式也可以通过把函数作为参数传递而简化。如下展示了一般的命令模式的 UML 类图:
各个命令可以有不同的接收者,即实现操作的对象
命令模式的目的是解耦调用操作的对象(调用者)和提供实现的对象(接收者)
这个模式的做法是,在二者之间放一个 Command 对象,让它实现只有一个方法(execute)的接口,调用接收者中的方法执行所需的操作
这样,调用者无须了解接收者的接口,而且不同的接收者可以适应不同的 Command 子类
命令模式是回调机制的面向对象替代品。可以不为调用者提供 Command 实例,而是给它一个函数。此时,调用者不调用 command.execute(),而是直接调用 command()。如下是一个简答的 Command 类,它的实例就是可调用对象:
1 2 3 4 5 6 7 8 9 class MacroCommand : """一个执行一组命令的命令""" def __init__ (self, commands ): self.commands = list (commands) def __call__ (self ): for command in self.commands: command()
复杂的命令模式(例如支持撤销操作)可能需要的不仅仅是简单的回调函数。即便如此,也可以考虑使用 Python 提供的几个替代品:
类似于 MacroCommand 那样的可调用实例,可以保存任何所需的状态,除了 __call__,还可以提供其他方法
可以使用闭包在调用之间保存函数的内部状态
很多情况下,在 Python 中使用函数或可调用对象实现回调更自然。有时,设计模式或 API 要求组件实现单方法接口,而该方法有一个很宽泛的名称,例如 execute、run 或 do_it。在 Python 中,这些模式或 API 通常可以使用作为一等对象的函数实现,从而减少样板代码。