流暢的python——6 使用一等函式實現設計模式
六、使用一等函式實現設計模式
符合模式並不表示做得對。 ——Ralph Johnson
經典的《設計模式:可複用面向物件軟體的基礎》的作者之一
策略模式
訂單 order
折扣策略,比如有三種
Promotion是策略基類,下面三個是具體實現的折扣策略
上下文
把一些計算委託給實現不同演算法的可互換元件,它提供服務。在這個電商示例中,上下文是 Order,它會根據不同的演算法計算促銷折扣。
策略
實現不同演算法的元件共同的介面。在這個示例中,名為 Promotion 的抽象類扮演這個角色。
具體策略
“策略”的具體子類。fidelityPromo、BulkPromo 和 LargeOrderPromo 是這裡實現的三個具體策略。
按照《設計模式:可複用面向物件軟體的基礎》一書的說明,具體策略由上下文類的客戶選擇。在這個示例中,例項化訂單之前,系統會以某種方式選擇一種促銷折扣策略,然後把它傳給 Order 構造方法。具體怎麼選擇策略,不在這個模式的職責範圍內。
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 = '<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 >= 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
使用函式實現 策略 模式
之前的例子,每個類只實現了一個方法,而且沒有物件屬性,所以,可以替換成三個策略函式。不再使用類。
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 = '<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 >= 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
值得注意的是,《設計模式:可複用面向物件軟體的基礎》一書的作者指出:“策略物件通常是很好的享元(flyweight)。”
那本書的另一部分對“享元”下了定義:“享元是可共享的物件,可以同時在多個上下文中使用。”
共享是推薦的做法,這樣不必在每個新的上下文(這裡是 Order 例項)中使用相同的策略時不斷新建具體策略物件,從而減少消耗。因此,為了避免“策略”模式的一個缺點(執行時消耗),《設計模式:可複用面向物件軟體的基礎》的作者建議再使用另一個模式。但此時,程式碼行數和維護成本會不斷攀升。
在複雜的情況下,需要具體策略維護內部狀態時,可能需要把“策略”和“享元”模式結合起來。但是,具體策略一般沒有內部狀態,只是處理上下文中的資料。此時,一定要使用普通的函式,別去編寫只有一個方法的類,再去實現另一個類宣告的單函式介面。函式比使用者定義的類的例項輕量,而且無需使用“享元”模式,因為各個策略函式在 Python 編譯模組時只會建立一次。普通的函式也是“可共享的物件,可以同時在多個上下文中使用”。
選擇最佳策略:簡單的方式
best_promo 函式的實現特別簡單,如示例 6-6 所示。
best_promo 迭代一個函式列表,並找出折扣額度最大的
promos = [fidelity_promo, bulk_item_promo, large_order_promo]
def best_promo(order):
"""選擇可用的最佳折扣"""
return max(promo(order) for promo in promos)
找出模組中的全部策略
globals()
返回一個字典,表示當前的全域性符號表。這個符號表始終針對當前模組(對函式或方法來說,是指定義它們的模組,而不是呼叫它們的模組)。
promos = [globals()[name] for name in globals()
if name.endswith('_promo') # 取 _promo 結尾的名稱
and name != 'best_promo'] # 防止無限遞迴
def best_promo(order):
"""選擇可用的最佳折扣
"""
return max(promo(order) for promo in promos)
inspect.getmembers
收集所有可用促銷的另一種方法是,在一個單獨的模組 promotions
中儲存所有策略函式,把best_promo 排除在外。
import promotions
promos = [func for name, func in
inspect.getmembers(promotions, inspect.isfunction)]
def best_promo(order):
"""選擇可用的最佳折扣
"""
return max(promo(order) for promo in promos)
inspect.getmembers
函式用於獲取物件(這裡是 promotions 模組)的屬性,第二個引數是可選的判斷條件(一個布林值函式)。我們使用的是 inspect.isfunction
,只獲取模組中的函式。
不管怎麼命名策略函式,示例都可用;唯一重要的是,promotions 模組只能包含計算訂單折扣的函式。當然,這是對程式碼的隱性假設。如果有人在 promotions 模組中使用不同的簽名定義函式,那麼 best_promo 函式嘗試將其應用到訂單上時會出錯。
命令模式
“命令”設計模式也可以通過把函式作為引數傳遞而簡化。
選單驅動的文字編輯器的 UML 類圖,使用“命令”設計模式實現。各個命令可以有不同的接收者(實現操作的物件)。對 PasteCommand 來說,接收者是Document。對 OpenCommand 來說,接收者是應用程式
“命令”模式的目的是解耦呼叫操作的物件(呼叫者)和提供實現的物件(接收者)。
這個模式的做法是,在二者之間放一個 Command 物件,讓它實現只有一個方法(execute)的介面,呼叫接收者中的方法執行所需的操作。這樣,呼叫者無需瞭解接收者的介面,而且不同的接收者可以適應不同的Command 子類。呼叫者有一個具體的命令,通過呼叫 execute 方法執行。注意,圖中的 MacroCommand 可能儲存一系列命令,它的 execute() 方法會在各個命令上呼叫相同的方法。
Gamma 等人說過:“命令模式是回撥機制的面向物件替代品。”問題是,我們需要回調機制的面向物件替代品嗎?有時確實需要,但並非始終需要。
我們可以不為呼叫者提供一個 Command 例項,而是給它一個函式。此時,呼叫者不用呼叫 command.execute(),直接呼叫 command() 即可。MacroCommand 可以實現成定義了__call__
方法的類。這樣,MacroCommand 的例項就是可呼叫物件,各自維護著一個函式列表,供以後呼叫。
class MacroCommand:
"""一個執行一組命令的命令"""
def __init__(self, commands):
self.commands = list(commands) # 使用 commands 引數構建一個列表,這樣能確保引數是可迭代物件,還能在各個 MacroCommand 例項中儲存各個命令引用的副本。
def __call__(self):
for command in self.commands:
command()
可以使用閉包在呼叫之間儲存函式的內部狀態。
把實現單方法介面的類的例項替換成可呼叫物件。畢竟,每個Python 可呼叫物件都實現了單方法介面,這個方法就是 __call__