《Fluent Python》- 06 使用一等函数实现设计模式

这节主要说明使用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__。

posted @ 2020-04-27 16:26  C`Moriarty  阅读(224)  评论(0编辑  收藏  举报
/* 鼠标点击求赞文字特效 */ /*鼠标跟随效果*/