《流暢的Python》筆記。
本篇主要講述Python中使用函式來實現策略模式和命令模式,最後總結出這種做法背後的思想。
1. 重構策略模式
策略模式如果用物件導向的思想來簡單解釋的話,其實就是“多型”。父類指向子類,根據子類對同一方法的不同重寫,得到不同結果。
1.1 經典的策略模式
下圖是經典的策略模式的UML類圖:
《設計模式:可複用物件導向軟體的基礎》一書這樣描述策略模式:
定義一系列演算法,把它們封裝起來,且使它們能相互替換。本模式使得演算法可獨立於使用它的客戶而變化。
下面以一個電商打折的例子來說明策略模式,打折方案如下:
- 有1000及以上積分的顧客,每個訂單享5%優惠;
- 同一訂單中,每類商品的數量達到20個及以上時,該類商品享10%優惠;
- 訂單中的不同商品達10個及以上時,整個訂單享7%優惠。
為此我們需要建立5個類:
Order
類:訂單類,相當於上述UML圖中的Context
上下文;Promotion
類:折扣類的父類,相當於UML圖中的Strategy
策略類,實現不同策略的共同介面;- 具體策略類:
FidelityPromo
,BulkPromo
和LargeOrderPromo
依次對應於上述三個打折方案。
以下是經典的策略模式在Python中的實現:
from abc import ABC, abstractmethod
from collections import namedtuple
Customer = namedtuple("Customer", "name fidelity")
class LineItem: # 單個商品
def __init__(self, product, quantity, price):
self.produce = 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) # 形參cart中的元素是LineItem
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
class Promotion(ABC): # 策略:抽象基類
@abstractmethod # 抽象方法
def discount(self, order):
"""返回折扣金額(正值)"""
class FidelityPromo(Promotion): # 第一個具體策略
"""積分1000及以上的顧客享5%"""
def discount(self, order):
return order.total() * 0.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() * 0.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() * 0.07
return 0
複製程式碼
該類的使用示例如下:
>>> ann = Customer("Ann Smith", 1100)
>>> joe = Customer("John Joe", 0)
>>> cart = [LineItem("banana", 4, 0.5), LineItem("apple", 10, 1.5),
... LineItem("watermellon", 5, 5.0)]
>>> Order(ann, cart, FidelityPromo()) # 每次新建一個具體策略類
>>> Order(joe, cart, FidelityPromo())
複製程式碼
1.2 Python函式重構策略模式
現在用Python函式以更少的程式碼來重構上述的策略模式,去掉了抽象類Promotion
,用函式代替具體的策略類:
# 不用匯入abc模組,去掉了Promotion抽象類;
# Customer, LineItem不變,Order類只修改due()函式;三個具體策略類改為函式
-- snip --
class Order:
-- snip --
def due(self): # 折扣
if self.promotion is None:
discount = 0
else:
discount = self.promotion(self) # 修改為函式
return self.total() - discount
def fidelity_promo(order):
"""積分1000及以上的顧客享5%"""
return order.total() * 0.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() * 0.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() * 0.07
return 0
複製程式碼
該類現在的使用示例如下:
>>> Order(ann, cart, fidelity_promo) # 沒有例項化新的促銷物件,函式拿來即用
複製程式碼
脫離Python語言環境,從面相物件程式設計來說:
1.1中的使用示例可以看出,每次建立Order
類時,都建立了一個具體策略類,即使不同的訂單都用的同一個策略。按理說它們應該共享同一個具體策略的例項,但實際並沒有。這就是策略模式的一個弊端。為了彌補這個弊端,如果具體的策略沒有維護內部狀態,你可以為每個具體策略建立一個例項,然後每次都傳入這個例項,這就是單例模式;但如果要維護內狀態,就需要將策略模式和享元模式結合使用,這又提高了程式碼行數和維護成本。
在Python中則可以用函式來避開策略模式的這些弊端:
- 不用維護內部狀態時,我們可以直接用一般的函式;如果需要維護內部狀態,可以編寫裝飾器(裝飾器也是函式);
- 相對於編寫一個抽象類,再實現這個抽象類的介面來說,直接編寫函式更方便;
- 函式比使用者定義的類的例項更輕量;
- 無需去實現享元模式,每個函式在Python編譯模組時只會建立一次,函式本身就是可共享的物件。
1.3 自動選擇最佳策略
上述程式碼中,我們需要自行傳入打折策略,但我們更希望的是程式自動選擇最佳打折策略。以下是我們最能想到的一種方式:
# 在生成Order例項時,傳入一個best_promo函式,讓其自動選擇最佳策略
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"] # 自動獲取當前模組中的打折函式
def best_promo(order):
-- snip --
複製程式碼
在Python中,模組也是一等物件,globals()
函式是標準庫提供的處理模組的函式,它返回一個字典,表示當前全域性符號表。這個符號表始終針對當前模組(對函式或方法來說,是指定義它們的模組,而不是呼叫它們的模組)
如果我們把各種具體策略單獨放到一個模組中,比如放到promotions
模組中,上述程式碼還可改為如下形式:
# 各具體策略單獨放到一個模組中
import promotions, inspect
# inspect.getmembers函式用於獲取物件的屬性,第二個引數是可選的判斷條件
promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
-- snip --
複製程式碼
其實,動態收集具體策略函式更為顯式的一種方案是使用簡單的裝飾器,這將在下一篇中介紹。
2. 命令模式
命令模式的UML類圖如下:
命令模式的目的是解耦發起呼叫的物件(呼叫者,Caller
)和提供實現的物件(接受者,Receiver
)。實際做法就是在它們之間增加一個命令類(Command
),它只有一個抽象介面execute()
,具體命令類實現這個介面即可。這樣呼叫者就無需瞭解接受者的介面,不同的接受者還可以適應不同的Command
子類。
有人說“命令模式是回撥機制的物件導向替代品”,但問題是,Python中我們不一定需要這個替代品。具體說來,我們可以不為呼叫者提供一個Command
例項,而是給它一個函式。此時,呼叫者不用呼叫command.execute()
,而是直接command()
。
以下是一般的命令模式程式碼:
from abc import ABC, abstractmethod
class Caller:
def __init__(self, command=None):
self.command = command
def action(self):
"""把對接受者的呼叫交給中介Command"""
self.command.execute()
class Receiver:
def do_something(self):
"""具體的執行命令"""
print("I'm a receiver")
class Command(ABC):
@abstractmethod
def execute(self):
"""呼叫具體的接受者方法"""
class ConcreteCommand(Command):
def __init__(self, receiver):
self.receiver = receiver
def execute(self):
self.receiver.do_something()
if __name__ == "__main__":
receiver = Receiver()
command = ConcreteCommand(receiver)
caller = Caller(command)
caller.action()
# 結果:
I'm a receiver
複製程式碼
直接將上述程式碼改成函式的形式,其實並不容易改寫,因為具體的命令類還儲存了接收者。但是換個思路,將其改成可呼叫物件,那麼程式碼就可以變成如下形式:
class Caller:
def __init__(self, command=None):
self.command = command
def action(self):
# 之前是self.command.execute()
self.command()
class Receiver:
def do_something(self):
"""具體的執行命令"""
print("I'm a receiver")
class ConcreteCommand:
def __init__(self, receiver):
self.receiver = receiver
def __call__(self):
self.receiver.do_something()
if __name__ == "__main__":
receiver = Receiver()
command = ConcreteCommand(receiver)
caller = Caller(command)
caller.action()
複製程式碼
3. 總結
看完這兩個例子,不知道大家發現了什麼相似之處了沒有:
它們都把實現單方法介面的類的例項替換成了可呼叫物件。畢竟,每個Python可呼叫物件都實現了單方法介面,即__call__
方法。
直白一點說就是,如果你定義了一個抽象類,這個類只有一個抽象方法a()
,然後還要為這個抽象類派生出一大堆具體類來重寫這個方法a()
,那麼此時大可不必定義這個抽象類,直接將這些具體類改寫成可呼叫物件即可,在__call__
方法中實現a()
要實現的功能。
這相當於用Python中可呼叫物件的基類充當了我們定義的基類,我們便不用再定義基類;對抽象方法a()
的重寫變成了對特殊方法__call__
的重寫,畢竟我們只是想要這些方法有一個相同的名字,至於叫什麼其實無所謂。
迎大家關注我的微信公眾號"程式碼港" & 個人網站 www.vpointer.net ~