流畅的python——6 使用一等函数实现设计模式

六、使用一等函数实现设计模式

符合模式并不表示做得对。 ——Ralph Johnson

​ 经典的《设计模式:可复用面向对象软件的基础》的作者之一

策略模式

订单 order
折扣策略,比如有三种

Promotion是策略基类,下面三个是具体实现的折扣策略

上下文

  把一些计算委托给实现不同算法的可互换组件,它提供服务。在这个电商示例中,上下文是 Order,它会根据不同的算法计算促销折扣。

策略

  实现不同算法的组件共同的接口。在这个示例中,名为 Promotion 的抽象类扮演这个角色。

具体策略

  “策略”的具体子类。fidelityPromo、BulkPromo 和 LargeOrderPromo 是这里实现的三个具体策略。

按照《设计模式:可复用面向对象软件的基础》一书的说明,具体策略由上下文类的客户选择。在这个示例中,实例化订单之前,系统会以某种方式选择一种促销折扣策略,然后把它传给 Order 构造方法。具体怎么选择策略,不在这个模式的职责范围内。

from abc import ABC, abstractmethod
from collections import namedtuple

Customer = namedtuple('Customer', 'name fidelity')

class LineItem:
     def __init__(self, product, quantity, price):
     self.product = product
     self.quantity = quantity
     self.price = price
     def total(self):
     return self.price * self.quantity

class Order: # 上下文
     def __init__(self, customer, cart, promotion=None):
     self.customer = customer
     self.cart = list(cart)
     self.promotion = promotion
     def total(self):
     if not hasattr(self, '__total'):
     self.__total = sum(item.total() for item in self.cart)
     return self.__total
     def due(self):
     if self.promotion is None:
     discount = 0
     else:
     discount = self.promotion.discount(self)
     return self.total() - discount
     def __repr__(self):
     fmt = '<Order total: {:.2f} due: {:.2f}>'
     return fmt.format(self.total(), self.due())
    
class Promotion(ABC) : # 策略:抽象基类
     @abstractmethod
     def discount(self, order):
     """返回折扣金额(正值)"""
    
class FidelityPromo(Promotion): # 第一个具体策略
     """为积分为1000或以上的顾客提供5%折扣"""
     def discount(self, order):
     return order.total() * .05 if order.customer.fidelity >= 1000 else 0

class BulkItemPromo(Promotion): # 第二个具体策略
     """单个商品为20个或以上时提供10%折扣"""
     def discount(self, order):
     discount = 0
     for item in order.cart:
     if item.quantity >= 20:
     discount += item.total() * .1
     return discount
    
class LargeOrderPromo(Promotion): # 第三个具体策略
     """订单中的不同商品达到10个或以上时提供7%折扣"""
     def discount(self, order):
     distinct_items = {item.product for item in order.cart}
     if len(distinct_items) >= 10:
     return order.total() * .07
     return 0

使用函数实现 策略 模式

之前的例子,每个类只实现了一个方法,而且没有对象属性,所以,可以替换成三个策略函数。不再使用类。

from collections import namedtuple

Customer = namedtuple('Customer', 'name fidelity')

class LineItem:
     def __init__(self, product, quantity, price):
     self.product = product
     self.quantity = quantity
     self.price = price
     def total(self):
     return self.price * self.quantity

class Order: # 上下文
     def __init__(self, customer, cart, promotion=None):
     self.customer = customer
     self.cart = list(cart)
     self.promotion = promotion
     def total(self):
     if not hasattr(self, '__total'):
     self.__total = sum(item.total() for item in self.cart)
     return self.__total
     def due(self):
     if self.promotion is None:
     discount = 0
     else:
     discount = self.promotion(self)
     return self.total() - discount
     def __repr__(self):
     fmt = '<Order total: {:.2f} due: {:.2f}>'
     return fmt.format(self.total(), self.due())

def fidelity_promo(order):
     """为积分为1000或以上的顾客提供5%折扣"""
     return order.total() * .05 if order.customer.fidelity >= 1000 else 0

def bulk_item_promo(order):
     """单个商品为20个或以上时提供10%折扣"""
     discount = 0
     for item in order.cart:
     if item.quantity >= 20:
     discount += item.total() * .1
     return discount
    
def large_order_promo(order):
     """订单中的不同商品达到10个或以上时提供7%折扣"""
     distinct_items = {item.product for item in order.cart}
     if len(distinct_items) >= 10:
     return order.total() * .07
     return 0

值得注意的是,《设计模式:可复用面向对象软件的基础》一书的作者指出:“策略对象通常是很好的享元(flyweight)。”

那本书的另一部分对“享元”下了定义:“享元是可共享的对象,可以同时在多个上下文中使用。”

共享是推荐的做法,这样不必在每个新的上下文(这里是 Order 实例)中使用相同的策略时不断新建具体策略对象,从而减少消耗。因此,为了避免“策略”模式的一个缺点(运行时消耗),《设计模式:可复用面向对象软件的基础》的作者建议再使用另一个模式。但此时,代码行数和维护成本会不断攀升。

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

选择最佳策略:简单的方式

best_promo 函数的实现特别简单,如示例 6-6 所示。

best_promo 迭代一个函数列表,并找出折扣额度最大的

promos = [fidelity_promo, bulk_item_promo, large_order_promo]
def best_promo(order):
 """选择可用的最佳折扣"""
	return max(promo(order) for promo in promos)

找出模块中的全部策略

globals()

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

promos = [globals()[name] for name in globals()
          if name.endswith('_promo')  # 取 _promo 结尾的名称
          and name != 'best_promo']  # 防止无限递归
def best_promo(order):
    """选择可用的最佳折扣
     """
    return max(promo(order) for promo in promos)

inspect.getmembers

收集所有可用促销的另一种方法是,在一个单独的模块 promotions 中保存所有策略函数,把best_promo 排除在外。

import promotions
promos = [func for name, func in
          inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
    """选择可用的最佳折扣
     """
    return max(promo(order) for promo in promos)

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

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

命令模式

“命令”设计模式也可以通过把函数作为参数传递而简化。

菜单驱动的文本编辑器的 UML 类图,使用命令设计模式实现。各个命令可以有不同的接收者(实现操作的对象)。对 PasteCommand 来说,接收者是Document。对 OpenCommand 来说,接收者是应用程序

“命令”模式的目的是解耦调用操作的对象(调用者)和提供实现的对象(接收者)。

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

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

我们可以不为调用者提供一个 Command 实例,而是给它一个函数。此时,调用者不用调用 command.execute(),直接调用 command() 即可。MacroCommand 可以实现成定义了__call__ 方法的类。这样,MacroCommand 的实例就是可调用对象,各自维护着一个函数列表,供以后调用。

class MacroCommand:
    """一个执行一组命令的命令"""
    def __init__(self, commands):
        self.commands = list(commands)  # 使用 commands 参数构建一个列表,这样能确保参数是可迭代对象,还能在各个 MacroCommand 实例中保存各个命令引用的副本。
    def __call__(self):
        for command in self.commands:
            command()

可以使用闭包在调用之间保存函数的内部状态。

把实现单方法接口的类的实例替换成可调用对象。毕竟,每个Python 可调用对象都实现了单方法接口,这个方法就是 __call__

posted @ 2021-09-27 19:09  pythoner_wl  阅读(77)  评论(0编辑  收藏  举报