这节主要说明使用Python的一等函数实现一些设计模式(主要是策略模式)
经典的“策略”模式
《设计模式:可复用面向对象软件的基础》一书是这样概述“策略”模式的:
定义一些列算法,把它们一一封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。
接下来用电商中的“折扣”来简单模拟一下,折扣规则如下:
- 有1000或者以上积分的顾客,每个订单享5%的折扣。
- 同一订单中,单个商品的数量达到20个或以上,享10%折扣 。
- 订单中的不同商品达10个以上,享7%折扣。
“策略”模式里,涉及以下内容:
上下文:
把一些计算委托给实现不同算法的可互换组件,它提供服务。在这个电商示例中,上下文是Order,它会根据不同的算法计算促销折扣
策略:
实现不同算法的组件公共接口。这里示例里是Promotion。
具体策略:
“策略”的具体子类。示例就是图中下面那三个
代码实现:
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 = 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 >= 0: discount += item.total() * .1 return discount class LargeOrderPromo(Promotion): # 第三个 def discount(self, order): distinct_items = {item.product for item in order.cart} if len(distinct_items) >= 10: return order.total() * .07 return 0
我们这里把Promotion定义为抽象基类(ABC),主要是为了使用@abstractmethod装饰器。具体演示就不演示了,这里我们采用的是将Python视为对象的方式,下面展示去使用更少代码的方式。
使用函数实现“策略”模式
在上面的那个例子里,我们每个具体策略都是一个类,而且都定义了一个方法discount。此外我们的策略是没有转态的,没有属性。下面采用基于函数的方式实现:
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 = 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 >= 0: discount += item.total() * .1 return discount def large_order_promo(order): # 第三个 distinct_items = {item.product for item in order.cart} if len(distinct_items) >= 10: return order.total() * .07 return 0
与使用对象看上去大同小异,我们在使用的时候就只需要传函数而没必要在去实例化新促销对象。
在《设计模式:可复用面向对象软件的基础》中,作者说到:策略对象通常是很好的享元(享元就是可共享对象,可以在多个上下文中使用),这样就不必要在使用相同策略时反复创建对象。所以作者建议再使用另一个模式。但这样代码的维护成本会不断上升。在复杂的情况下,需要具体策略维护内部转态时,可能需要把“策略”和“享元”模式结合,但是具体策略通常是没有内部状态的,只是处理上下文数据。此时,一定要使用普通的函数,别去编写只有一个方法的类,再去实现一个另一个类声明的单函数接口。在Python中,普通函数,也是“可共享对象,可以同时在多个上下文中使用”。
选择策略
我们需要一个方法帮助顾客选取最佳的策略:
promos = [fidelity_promo, bulk_item_promo, large_order_promo] def best_promo(order): return max(promo(order) for promo in promos)
上面代码易于阅读,但是维护就得注意,因为如果新增加了具体策略,promos就一定得手动添加。否则取最大时是不会考虑的。
那么我们就需要解决这个问题,需要想办法找出模块中的所有策略:
promos = [globals()[name] for name in globals() if name.endswith('_promo') and name != 'best_promo'] # 推导方式,不用手动输入
我们去寻找'_promo'结尾的函数,这样我们就必须规范命名规则。
当然,我们也可以简单的采用高阶内省函数:
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction())] # 注意一下,inspect模块是自己导入的。promotions模块就是你定义的存放具体策略的模块
inspect.getmembers 是获取对象的属性(这里就是promotions),第二个参数是可选判断条件。我们这个参数只是获取模块中的函数,这里我们就可以自由命名了,要注意的是,promotions里面只能放具体策略。
最后还有一个命令模式,我这里就不赘述了,和策略模式类似,核心思想还是把类换成函数。Python提供了一个很好的函数就是__call__。