1. 程式人生 > 其它 >流暢的python——6 使用一等函式實現設計模式

流暢的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__