在阅读《Fluent Python》中关于设计模式的部分之前,我是坚信设计模式是语言无关的,至少在大部分常用编程语言中都可以比较好的运用。
在读完《Fluent Python》的相关章节后,我的想法有所改变,虽然经典的设计模式的确可以在大多数编程语言中实现,但是对于一些独特的语言,他们有更恰当更优雅的实现方式。
当然,这其中包括Python。
策略模式
我曾经在中介绍过策略模式,并尝试用Python实现经典的策略模式。
但是为了示例的一致性和连续性,我会在这里用《Fluent Python》一书中的订单系统示例来重新实现一遍。
我会使用EA进行快速构建,想了解EA如何通过类图自动化生成Python框架代码的,可以阅读。
经典实现
简单解释一下这个订单系统,订单(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具有函数式变成的特性,这点是和纯面向对象语言所不同的,我们正可以利用这个特性进行简化,让策略模式变得更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》称之为“内省”。
内省
在当前这个示例中,我们可以通过两种途径实现内省:global
和inspect
。
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
。
事实上我们在中介绍过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关系图:
命令模式的目的是将命令的调用者与命令的接收者进行解耦。
用上面UML图进行说明:图中的菜单(Menu)是命令的调用者,通过它可以调用打开(OpenCommand)、粘贴(PasteCommand)、宏(MacroCommand)等具体命令,而文件(Document)和程序(Application)就是具体命令的接受者(执行者)。
可以很容易发现,通过这种模式,作为调用者的菜单完全不用持有具体执行者的句柄,它只需要持有相应的命令即可,这就实现了解耦。
我们可以如同之前对待策略模式一般,用Python式的方式思考如何简化。
Python方式
我们可以看到,对于具体命令,都是单方法,而且类似execute
这种命令毫无意义,其根本目的无非是提供一个可执行对象,至于其中方法名是execute
或者call
抑或是匿名,完全不重要。
这种可行行对象正是我们Python中的函数对象。
这种改写非常简单,这里不一一展示。
需要注意的是,对于简单命令我们都可以用函数替换,但宏(MacroCommand)不行。
我们都知道,宏命令是一组命令的集合,也就是说每个宏命令自身持有一个命令列表,执行一个宏命令就等于执行了一组命令。
用OOP的思想来说就是宏命令不仅有方法,还持有数据。
所以并不能简单用函数进行替换,但我们在中介绍过可执行类实例,它可以完全满足宏的要求:可持有对象、可执行。
class MacroCommand():
def __init__(self, commands:list):
self.commands = commands
def __call__(self):
for command in self.commands:
command()
macroCommand = MacroCommand()
macroCommand()
总结
好了,我们现在总结一下经典设计模式与Python方式的不同。
-
在Python中,我们并非一定需要实现接口、抽象基类。因为Python这种动态语言实现多态并不依赖于抽象基类和接口。
-
对于单方法类,我们可以考虑使用函数对象进行替换,尤其是类似命令模式中包含
execute
这种无意义方法名的情况。 -
对于持有数据的单方法类,我们可以考虑用可调用类对象进行替换。
最后要说的是,Python式的设计模式的确更为简练和优雅,但相应的,扩展性也会稍差,比如需要持有数据或者新增方法等情况出现。但是,如果你对商业开发有一定经验就会知道,根本不存在设计初期就考虑可扩展性的情况,那完全是一种妄想,程序往往会在用户持续不断匪夷所思的需求上变得面目全非,所以为了可扩展性而添加一些不必要的代码和结构完全是不必要的,至少不需要花大力气在那上面。从这个角度上来说,如果能通过Python式的设计模式减少大量框架代码的开发,是很有意义的。
好了,以上,谢谢阅读。
本来以为只需要一小会就可以完成的文章又花了一个下午。
最后附上思维导图:
参考资料
文章评论