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


博主在大二上软件设计模式相关课程的时候,对动态语言本身了解不够深刻,因此在参考别人的博客下写了共二十余篇用python语言实现23种设计模式的博客,现在看来有些确实不太恰当。一些特殊的面向对象语言可以直接支持我们的某些模式。

一、策略模式

1、 参考博客

python-策略模式,在博主的这篇博客种大致介绍了策略模式的相关简介,代码实现,优缺点,适应场景

2 、实例

策略模式的概述:定义一系列算法,把它们一一封装起来,并且使它们可以相互替换。本模式使得算法可以独立于使用它的客户而变化。
那么可以在电商平台上做一个系统使用策略模式对相关的用户订单进行折扣。

使用“策略”设计模式处理订单折扣的 UML 类图
假如一个网店制定了下述折扣规则。(可以理解为三种算法)

  • 有 1000 或以上积分的顾客,每个订单享 5% 折扣。
  • 同一订单中,单个商品的数量达到 20 个或以上,享 10% 折扣。
  • 订单中的不同商品达到 10 个或以上,享 7% 折扣

上下文:把一些计算委托给实现不同算法的可互换组件,它提供服务。在这个电商示例中,上下文是 Order,它会根据不同的算法计算促销折扣。
策略:实现不同算法的组件共同的接口。在这个示例中,名为 Promotion的抽象类扮演这个角色。
具体策略:“策略”的具体子类。fidelityPromo、BulkPromo 和LargeOrderPromo 是这里实现的三个具体策略。

代码实现

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 = ''
        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

class Promotion(ABC) 把 Promotion 定义为抽象基类(Abstract BaseClass,ABC),这么做是为了使用 @abstractmethod 装饰器,从而明确表明所用的模式,
在 Python 3.4 中,声明抽象基类最简单的方式是子类化abc.ABC, 上面就是这么做的。从 Python 3.0 到 Python3.3,必须在 class 语句中使用 metaclass= 关键字(例如,class Promotion(metaclass=ABCMeta):)

使用不同的策略实现折扣

>>> joe = Customer('John Doe', 0) ?
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5), ?
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, FidelityPromo()) ?

>>> Order(ann, cart, FidelityPromo()) ?

>>> banana_cart = [LineItem('banana', 30, .5), ?
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, BulkItemPromo()) ?

>>> long_order = [LineItem(str(item_code), 1, 1.0) ?
... for item_code in range(10)]
>>> Order(joe, long_order, LargeOrderPromo()) ?

>>> Order(joe, cart, LargeOrderPromo())

? 两个顾客:joe 的积分是 0,ann 的积分是 1100。
? 有三个商品的购物车。
? fidelityPromo 没给 joe 提供折扣。
? ann 得到了 5% 折扣,因为她的积分超过 1000。
? banana_cart 中有 30 把香蕉和 10 个苹果。
? BulkItemPromo 为 joe 购买的香蕉优惠了 1.50 美元。
? long_order 中有 10 个不同的商品,每个商品的价格为 1.00 美元。
? LargerOrderPromo 为 joe 的整个订单提供了 7% 折扣

3、使用函数重构策略模式

在2中每个策略类都只实现了一种策略,定义了一个方法封装了一种算法实现订单的折扣,下面用函数代替策略类实现

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 = ''
        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

在上一篇博客介绍了函数也是一等对象,在上面的实例中,将函数作为参数传递给其他的函数,这是函数作为一等对象的特性之一
使用函数实现的促销折扣的 Order 类示例

>>> joe = Customer('John Doe', 0) 
>>> ann = Customer('Ann Smith', 1100)
>>> cart = [LineItem('banana', 4, .5),
... LineItem('apple', 10, 1.5),
... LineItem('watermellon', 5, 5.0)]
>>> Order(joe, cart, fidelity_promo)  # 为了把折扣策略应用到 Order 实例上,只需把促销函数作为参数传入

>>> Order(ann, cart, fidelity_promo)

>>> banana_cart = [LineItem('banana', 30, .5),
... LineItem('apple', 10, 1.5)]
>>> Order(joe, banana_cart, bulk_item_promo) 

>>> long_order = [LineItem(str(item_code), 1, 1.0)
... for item_code in range(10)]
>>> Order(joe, long_order, large_order_promo)

>>> Order(joe, cart, large_order_promo)

二、命令模式

1、 参考博客
python-命令模式,在博主的这篇博客种大致介绍了策略模式的相关简介,代码实现,优缺点,适应场景

2、简介
“命令”设计模式也可以通过把函数作为参数传递而简化。“命令”模式的目的是解耦调用操作的对象(调用者)和提供实现的对象(接收者)。

菜单驱动的文本编辑器的 UML 类图,使用“命令”设计模式实现。各个命令可以有不同的接收者(实现操作的对象)。对PasteCommand 来说,接收者是 Document。对 OpenCommand 来说,接收者是应用程序。
这个模式的做法是,在二者之间放一个 Command 对象,让它实现只有一个方法(execute)的接口,调用接收者中的方法执行所需的操作。这样,调用者无需了解接收者的接口,而且不同的接收者可以适应不同的 Command 子类。调用者有一个具体的命令,通过调用 execute 方法执行。注意,图 6-2 中的 MacroCommand 可能保存一系列命令,它的execute() 方法会在各个命令上调用相同的方法。
可以不为调用者提供一个 Command 实例,而是给它一个函数。此时,调用者不用调用 command.execute(),直接调用 command() 即可。MacroCommand 可以实现成定义了 call 方法的类。这样,MacroCommand 的实例就是可调用对象,各自维护着一个函数列表,供以后调用,如下:
MacroCommand 的各个实例都在内部存储着命令列表

class MacroCommand:
"""一个执行一组命令的命令"""
    def __init__(self, commands):
        self.commands = list(commands) # ?
    def __call__(self):
        for command in self.commands: # ?
            command()

? 使用 commands 参数构建一个列表,这样能确保参数是可迭代对象,还能在各个 MacroCommand 实例中保存各个命令引用的副本。
? 调用 MacroCommand 实例时,self.commands 中的各个命令依序执行。在前面说过自定义的类只要实现特殊方法__call__,就可以作为可调用的对象

三、总结

回过头来看,在重构的策略模式中,将原本单方法的策略类简化成单个函数对象,减少了代码量,在命令模式中,将MacorCommand类实现特殊方法__call__,其实例化对象就是可直接调用。两种模式类似:都是把实现单方法接口的类的实例替换成可调用对象。