【經典案例】Python詳解設計模式:策略模式

Python程式設計時光發表於2019-04-22

完成一項任務往往有多種方式,我們將其稱之為策略。

比如,超市做活動,如果你的購物積分滿1000,就可以按兌換現金抵用券10元,如果購買同一商品滿10件,就可以打9折,如果如果購買的金額超過500,就可以享受滿減50元的優惠。這是三個不同的促銷策略。

再比如,聯絡朋友、同學,可以打電話,也可以發簡訊,可以發微信,也可以發郵件,這是四個不同的聯絡策略。

再比如,去外出旅遊,我們可以選擇火車,也可以選擇公共汽車,可以選擇飛機,也可以選擇自駕遊。這又是四個不同的出行策略。

以上這些真實場景,都有策略選擇模型的影子,可以考慮使用策略模式。

經典的策略模式,是由三部分組成

  • Context:上下文環境類
  • Stragety:策略基類
  • ConcreteStragety:具體策略

【經典案例】Python詳解設計模式:策略模式

以第一個超市做活動的場景來舉個例子。

  • Context:Order類,訂單資訊,包括商品,價格和數量,以為購買者等
  • Stragety:Promotion類,抽象基類,包含一個抽象方法(計算折扣)
  • ContreteStragety:分三個類,FidelityPromo,BulkItemPromo,LargeOrderPromo,實現具體的折扣計算方法。

首先是 Order 類:

class Item:
    def __init__(self, issue, price, quantity):
        self.issue = issue
        self.price = price
        self.quantity = quantity

    def total(self):
        return self.price * self.quantity

class Order:
    def __init__(self, customer, promotion=None):
        self.cart = []
        self.customer = customer
        self.promotion = promotion

    def add_to_cart(self, *items):
        for item in items:
            self.cart.append(item)

    def total(self):
        total = 0
        for item in self.cart:
            total += item.total()

        return total

    def due(self):
        if not self.promotion:
            discount = 0
        else:
            discount  = self.promotion.discount(self)
        return (self.total() - discount)
複製程式碼

然後是積分兌換現金券的策略,為了保證我們的程式碼具有良好的可擴充套件性及維護性,我會先寫一個策略類,它是一個抽象基類,它的子類都是一個具體的策略,都必須實現 discount 方法,就比如我們們的積分兌換現金策略。

from abc import ABC, abstractmethod

class Promotion(ABC):
    @abstractmethod
    def discount(self, order):
        pass


class FidelityPromo(Promotion):
    '''
    如果積分滿1000分,就可以兌換10元現金券
    '''
    def discount(self, order):
        return 10 if order.customer.fidelity >1000 else 0
複製程式碼

假設現在小明去商場買了一件衣服(600塊),兩雙鞋子(200*2),他的購物積分有1500點。

在平時,商場一般都沒有活動,但是長年都有積分換現金抵用券的活動。

>>> from collections import namedtuple

# 定義兩個欄位:名字,購物積分
>>> Customer = namedtuple('Customer', 'name fidelity')
>>> xm = Customer('小明', 1500)
>>> item1 = Item('鞋子', 200, 3)
>>> item2 = Item('衣服', 600, 1)
>>> order = Order(xm, FidelityPromo())
>>> order.add_to_cart(item1, item2)

# 原價 1200,用上積分後,只要1190
>>> order
<Order Total:1200 due:1190>
複製程式碼

眼看著,五一節也快了,商場準備大搞促銷

  • 只要單項商品購買10件,即可9折。
  • 如果訂單總金額大於等於500,就可以立減50。

有了此前我們使用 策略模式 打下的基礎,我們並不是使用硬編碼的方式來配置策略,所以不需要改動太多的原始碼,只要直接定義五一節的兩個促銷策略類即可(同樣繼承自 Promotion 抽象基類),就像外掛一樣,即插即用。

class BulkItemPromo(Promotion):
    '''
    如果單項商品購買10件,即可9折。
    '''
    def discount(self, order):
        discount = 0
        for item in order.cart:
            if item.quantity >= 10:
                discount += item.total() * 0.1
        return discount

class LargeOrderPromo(Promotion):
    '''
    如果訂單總金額大於等於500,就可以立減50
    '''
    def discount(self, order):
        discount = 0
        if order.total() >= 500:
            discount = 50

        return discount
複製程式碼

看到商場活動如此給力,小明的錢包也鼓了起來,開始屯起了生活用品。

如果使用了第一個策略,原價600,只需要花 580

>>> from collections import namedtuple
>>> Customer = namedtuple('Customer', 'name fidelity')

>>> xm = Customer('小明', 300)

>>> item1 = Item('紙巾', 20, 10)
>>> item2 = Item('食用油', 50, 4)
>>> item3 = Item('牛奶', 50, 4)


>>> order = Order(xm, BulkItemPromo())
>>> order.add_to_cart(item1, item2, item3)

>>> order
<Order Total:600 due:580.0>
複製程式碼

如果使用了第二個策略,原價600,只需要花550

>>> from collections import namedtuple
>>> Customer = namedtuple('Customer', 'name fidelity')

>>> xm = Customer('小明', 300)

>>> item1 = Item('紙巾', 20, 10)
>>> item2 = Item('食用油', 50, 4)
>>> item3 = Item('牛奶', 50, 4)


>>> order = Order(xm, LargeOrderPromo())
>>> order.add_to_cart(item1, item2, item3)

>>> order
<Order Total:600 due:550>
複製程式碼

兩個策略即插即用,只需要在前臺下訂單時,選擇對應的策略即可,原業務邏輯無需改動。

>>> order = Order(xm, BulkItemPromo())
>>> order = Order(xm, LargeOrderPromo())
複製程式碼

但是問題很快又來了,商場搞活動,卻讓顧客手動選擇使用哪個優惠策略,作為一個良心的商家,應該要能自動對比所有策略得出最優惠的價格來給到顧客。這就要求後臺程式碼要能夠找出當前可用的全部策略,並一一比對摺扣。

# 找出所有的促銷策略
all_promotion = [globals()[name] for name in globals() if name.endswith('Promo') and name != 'BestPromo']

# 實現一個最優策略類
class BestPromo(Promotion):
    def discount(self, order):
        # 找出當前檔案中所有的策略
        all_promotion = [globals()[name] for name in globals() if name.endswith('Promo') and name != 'BestPromo']

        # 計算最大折扣
        return max([promo().discount(order) for promo in all_promotion])
複製程式碼

在前臺下訂單的時候,就會自動計算所有的優惠策略,直接告訴顧客最便宜的價格。

# 直接選擇這個最優策略
>>> order = Order(xm, BestPromo())
>>> order.add_to_cart(item1, item2, item3)

>>> order
<Order Total:600 due:550>
複製程式碼

通過以上例子,可以總結出使用策略模式的好處

  1. 擴充套件性優秀,移植方便,使用靈活。可以很方便擴充套件策略;
  2. 各個策略可以自由切換。這也是依賴抽象類設計介面的好處之一;

但同時,策略模式 也會帶來一些弊端。

  1. 專案比較龐大時,策略可能比較多,不便於維護;
  2. 策略的使用方必須知道有哪些策略,才能決定使用哪一個策略,這與迪米特法則是相違背的。

對於以上的例子,仔細一想,其實還有不少可以優化的地方。

比如,為了實現經典的模式,我們先要定義一個抽象基類,再實現具體的策略類。對於上面這樣一個簡單的計算折扣價格邏輯來說,其實可以用函式來實現,然後在例項化 Order 類時指定這個策略函式即可,大可不必將類給搬出來。這樣就可以避免在下訂單時,不斷的建立策略物件,減少多餘的執行時消耗。這裡就不具體寫出程式碼了。

所以學習設計模式,不僅要知道如何利用這樣的模式組織程式碼,更要領會其思想,活學活用,靈活變通。

以上,就是今天關於 策略模式 的一些個人分享,如有講得不到位的,還請後臺留言指正!

參考文件

  • 《流暢的Python》

關注公眾號,獲取最新干貨!

相關文章