Python学习笔记23:Python设计模式

Python学习笔记23:Python设计模式

在阅读《Fluent Python》中关于设计模式的部分之前,我是坚信设计模式是语言无关的,至少在大部分常用编程语言中都可以比较好的运用。

在读完《Fluent Python》的相关章节后,我的想法有所改变,虽然经典的设计模式的确可以在大多数编程语言中实现,但是对于一些独特的语言,他们有更恰当更优雅的实现方式

当然,这其中包括Python。

在接下来,我们会通过几个示例来比较经典的设计模式实现和Python式的实现,观察其中的差异。

策略模式

我曾经在设计模式 by Python1:策略模式中介绍过策略模式,并尝试用Python实现经典的策略模式。

但是为了示例的一致性和连续性,我会在这里用《Fluent Python》一书中的订单系统示例来重新实现一遍。

我会使用EA进行快速构建,想了解EA如何通过类图自动化生成Python框架代码的,可以阅读Enterprise Architect 15 使用指南

经典实现

image-20210420161327897

简单解释一下这个订单系统,订单(Order)中包含顾客(Customer)、购物车(Cart)和促销策略(Promotion),顾客包含姓名和积分(fidelity)。购物车中包含一个多个商品(Item)的列表。有三种促销策略,分别是针对顾客积分多少进行促销(FidelityPromo),对单个商品购买数量超过一定时进行促销(BulkItemPromo),购买不同种类的多个商品时候的促销(LargeOrderPromo)。

代码就不一一罗列了,整体打包上传到百度盘:

链接: https://pan.baidu.com/s/1ULUGepGg9k9Flrzig57syQ

提取码: 8wgr

我编写了一个“简单”的测试代码来测试这个用策略模式实现的订单系统:

from src.order_system_pkg.Customer import Customer
from src.order_system_pkg.Cart import Cart
from src.order_system_pkg.Item import Item
from src.order_system_pkg.Order import Order
from src.order_system_pkg.BulkitemPromo import BulkitemPromo
from src.order_system_pkg.FidelityPromo import FidelityPromo
from src.order_system_pkg.LargeOrderPromo import LargeOrderPromo
BrusLee = Customer('Brus Lee', 90)
JackChen = Customer('Jack Chen', 2000)
items = [Item('apple', 5, 10), Item('banana', 50, 6.7), Item('mobile', 1, 1000)]
cart1 = Cart(items)
bulkItemPromo = BulkitemPromo()
fidelityPromo = FidelityPromo()
largeOrderPromo = LargeOrderPromo()
order1 = Order(JackChen, cart1, bulkItemPromo)
print(order1)
order2 = Order(JackChen, cart1, fidelityPromo)
print(order2)
items = [Item("item_"+str(i), 1, 1) for i in range(1,11)]
cart2 = Cart(items)
order3 = Order(JackChen, cart2, largeOrderPromo)
print(order3)
# <Order total:1385.00 due:1351.50>
# <Order total:1385.00 due:1315.75>
# <Order total:10.00 due:9.30>

现在我们来使用Python的方式来简化策略模式的实现。

Python方式

首先我们应该注意到,策略模式的核心是把可以复用的几组“策略”分别进行封装

在纯面向对象语言中,这种封装往往是通过类和接口来实现的,其结果就是复杂的继承、实现关系。而正如之前在Python学习笔记22:函数式编程中说的那样,Python具有函数式变成的特性,这点是和纯面向对象语言所不同的,我们正可以利用这个特性进行简化,让策略模式变得更Python化、更优雅。

在当前这个案例中,作为“策略”的促销类Promotion仅仅实现了一个方法,我们可以简单称呼这种策略为“单方法策略“。

order类通过接受不同的策略来实现与具体策略的解耦,而针对具体的这种单方法策略,我们完全无需在Python中构建复杂的接口、类实现关系,我们可以用函数简单进行替代。

这里多次提到“单方法策略”,是为了强调使用函数式编程的方式进行简化或替代是有条件限制的,需要具体问题具体分析,不能无脑替换。

首先建立一个模块promotion_func.py存放促销策略:

from .Order import Order


def bulk_item_promo(order: Order):
    discount = 0
    for item in order.cart.items:
        if item.num > 20:
            discount += item.total()*0.1
    return discount


def fidelity_promo(order: Order):
    if order.customer.fidelity>=1000:
        return 0.05*order.total()
    else:
        return 0


def large_order_promo(order: Order):
    if len(order.cart.items) >= 10:
        return order.total()*0.07
    else:
        return 0

Order.py中修改策略的调用方式:

    def due(self):
        return self.total()-self.promotion(self)

修改测试程序:

from src.order_system_pkg.Customer import Customer
from src.order_system_pkg.Cart import Cart
from src.order_system_pkg.Item import Item
from src.order_system_pkg.Order import Order
from src.order_system_pkg import promotion_func
BrusLee = Customer('Brus Lee', 90)
JackChen = Customer('Jack Chen', 2000)
items = [Item('apple', 5, 10), Item(
    'banana', 50, 6.7), Item('mobile', 1, 1000)]
cart1 = Cart(items)
order1 = Order(JackChen, cart1, promotion_func.bulk_item_promo)
print(order1)
order2 = Order(JackChen, cart1, promotion_func.fidelity_promo)
print(order2)
items = [Item("item_"+str(i), 1, 1) for i in range(1, 11)]
cart2 = Cart(items)
order3 = Order(JackChen, cart2, promotion_func.large_order_promo)
print(order3)
# <Order total:1385.00 due:1351.50>
# <Order total:1385.00 due:1315.75>
# <Order total:10.00 due:9.30>

是不是精简了很多?

最妙的是我们的策略现在直接封装在可调用函数对象中,完全无需再创建类实例,直接传递函数对象即可。

关于策略模式的Python化基本上已经讨论完了,剩下一些细节我们还可以继续讨论。

最佳策略

经常的,对于策略模式,我们可能还需要解决一个最佳策略的问题,比如上边的订单系统,如果我们不想针对具体订单人为绑定促销策略,而是希望能自动判断哪种促销策略最优,然后采用,这种问题的解决方案我们称之为最佳策略。

我们可以通过一种简单方式实现:

promotion_func.py模块中添加一个最佳策略:

def best_promo(order: Order):
    return max(fidelity_promo(order), bulk_item_promo(order), large_order_promo(order))

修改测试程序:

order1 = Order(JackChen, cart1, promotion_func.best_promo)
print(order1)
order2 = Order(JackChen, cart1, promotion_func.best_promo)
print(order2)
items = [Item("item_"+str(i), 1, 1) for i in range(1, 11)]
cart2 = Cart(items)
order3 = Order(JackChen, cart2, promotion_func.best_promo)
print(order3)
# <Order total:1385.00 due:1315.75>
# <Order total:1385.00 due:1315.75>
# <Order total:10.00 due:9.30>

现在我们无需人为指定具体促销策略。

但是还有一个小瑕疵,如果新增一个促销策略,我们就必须修改best_promo,如果我们忘记了,就导致最佳策略存在bug。

那能不能“自动地”把新的促销策略加入我们的最佳策略实现中呢?

答案是可以。

这种嗅探程序内部结构的方式,《Fluent Python》称之为“内省”。

内省

在当前这个示例中,我们可以通过两种途径实现内省:globalinspect

global()函数可以输出当前上下文环境中的变量池。

如果你通过print(globals())promotion_func.py中查看,可以看到类似以下的输出:

 'Order': <class 'src.order_system_pkg.Order.Order'>,
 'bulk_item_promo': <function bulk_item_promo at 0x0000026BA1FACA60>, 
'fidelity_promo': <function fidelity_promo at 0x0000026BA1FACAF0>,
 'large_order_promo': <function large_order_promo at 0x0000026BA1FACB80>,
 'best_promo': <function best_promo at 0x0000026BA1FACC10>}

可以看到,函数名和函数对象都有,我们可以利用这个特性来实现内省。

def best_promo(order: Order):
    functions = globals()
    promotionFuncs = []
    for funcName,function in functions.items():
        if funcName.endswith('_promo') and funcName!='best_promo' and callable(function):
            promotionFuncs.append(function)
    return max(promoFunc(order) for promoFunc in promotionFuncs)

需要注意的是,我们在判断中加入了funcName!='best_promo'这是因为避免无限递归调用。

当然,你还可以通过把best_promo函数从promotion_func.py模块中搬离的方式来避免这种麻烦。

或者你可以通过另一种方式来实现内省——inspect

事实上我们在Python学习笔记22:函数式编程中介绍过inspect模块,它可以分析函数的签名构成,此外,它同样可以分析模块。

简单起见,直接在测试代码中利用inspect构建最佳策略。

from src.order_system_pkg.Customer import Customer
from src.order_system_pkg.Cart import Cart
from src.order_system_pkg.Item import Item
from src.order_system_pkg.Order import Order
from src.order_system_pkg import promotion_func
import inspect


def best_promo(order: Order):
    promotionFuncs = inspect.getmembers(promotion_func, inspect.isfunction)
    return max(promoFunc(order) for funcName, promoFunc in promotionFuncs)


BrusLee = Customer('Brus Lee', 90)
JackChen = Customer('Jack Chen', 2000)
items = [Item('apple', 5, 10), Item(
    'banana', 50, 6.7), Item('mobile', 1, 1000)]
cart1 = Cart(items)
order1 = Order(JackChen, cart1, best_promo)
print(order1)
order2 = Order(JackChen, cart1, best_promo)
print(order2)
items = [Item("item_"+str(i), 1, 1) for i in range(1, 11)]
cart2 = Cart(items)
order3 = Order(JackChen, cart2, best_promo)
print(order3)

# <Order total:1385.00 due:1315.75>
# <Order total:1385.00 due:1315.75>
# <Order total:10.00 due:9.30>

关于策略模式的Python改写已经全部介绍完毕了。

最终代码我上传到百度盘了:

链接: https://pan.baidu.com/s/1cfqUUnz4RpjpXOwn8veixA

提取码: mbpu

命令模式

经典实现

我们先来看命令模式经典实现的UML关系图:

image-20210420174315203

命令模式的目的是将命令的调用者与命令的接收者进行解耦。

用上面UML图进行说明:图中的菜单(Menu)是命令的调用者,通过它可以调用打开(OpenCommand)、粘贴(PasteCommand)、宏(MacroCommand)等具体命令,而文件(Document)和程序(Application)就是具体命令的接受者(执行者)。

可以很容易发现,通过这种模式,作为调用者的菜单完全不用持有具体执行者的句柄,它只需要持有相应的命令即可,这就实现了解耦。

我们可以如同之前对待策略模式一般,用Python式的方式思考如何简化。

Python方式

我们可以看到,对于具体命令,都是单方法,而且类似execute这种命令毫无意义,其根本目的无非是提供一个可执行对象,至于其中方法名是execute或者call抑或是匿名,完全不重要。

这种可行行对象正是我们Python中的函数对象。

这种改写非常简单,这里不一一展示。

需要注意的是,对于简单命令我们都可以用函数替换,但宏(MacroCommand)不行。

我们都知道,宏命令是一组命令的集合,也就是说每个宏命令自身持有一个命令列表,执行一个宏命令就等于执行了一组命令。

用OOP的思想来说就是宏命令不仅有方法,还持有数据。

所以并不能简单用函数进行替换,但我们在Python学习笔记22:函数式编程中介绍过可执行类实例,它可以完全满足宏的要求:可持有对象、可执行。

class MacroCommand():
    def __init__(self, commands:list):
        self.commands = commands

    def __call__(self):
        for command in self.commands:
            command()

macroCommand = MacroCommand()
macroCommand()

总结

好了,我们现在总结一下经典设计模式与Python方式的不同。

  1. 在Python中,我们并非一定需要实现接口、抽象基类。因为Python这种动态语言实现多态并不依赖于抽象基类和接口。
  2. 对于单方法类,我们可以考虑使用函数对象进行替换,尤其是类似命令模式中包含execute这种无意义方法名的情况。
  3. 对于持有数据的单方法类,我们可以考虑用可调用类对象进行替换。

最后要说的是,Python式的设计模式的确更为简练和优雅,但相应的,扩展性也会稍差,比如需要持有数据或者新增方法等情况出现。但是,如果你对商业开发有一定经验就会知道,根本不存在设计初期就考虑可扩展性的情况,那完全是一种妄想,程序往往会在用户持续不断匪夷所思的需求上变得面目全非,所以为了可扩展性而添加一些不必要的代码和结构完全是不必要的,至少不需要花大力气在那上面。从这个角度上来说,如果能通过Python式的设计模式减少大量框架代码的开发,是很有意义的。

好了,以上,谢谢阅读。

本来以为只需要一小会就可以完成的文章又花了一个下午。

最后附上思维导图:

image-20210420180911843

参考资料

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值