完成一項任務往往有多種方式,我們將其稱之為策略。
比如,超市做活動,如果你的購物積分滿1000,就可以按兌換現金抵用券10元,如果購買同一商品滿10件,就可以打9折,如果如果購買的金額超過500,就可以享受滿減50元的優惠。這是三個不同的促銷策略。
再比如,聯絡朋友、同學,可以打電話,也可以發簡訊,可以發微信,也可以發郵件,這是四個不同的聯絡策略。
再比如,去外出旅遊,我們可以選擇火車,也可以選擇公共汽車,可以選擇飛機,也可以選擇自駕遊。這又是四個不同的出行策略。
以上這些真實場景,都有策略選擇模型的影子,可以考慮使用策略模式。
經典的策略模式,是由三部分組成
- Context:上下文環境類
- Stragety:策略基類
- ConcreteStragety:具體策略
以第一個超市做活動的場景來舉個例子。
- 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>
複製程式碼
通過以上例子,可以總結出使用策略模式
的好處
- 擴充套件性優秀,移植方便,使用靈活。可以很方便擴充套件策略;
- 各個策略可以自由切換。這也是依賴抽象類設計介面的好處之一;
但同時,策略模式 也會帶來一些弊端。
- 專案比較龐大時,策略可能比較多,不便於維護;
- 策略的使用方必須知道有哪些策略,才能決定使用哪一個策略,這與迪米特法則是相違背的。
對於以上的例子,仔細一想,其實還有不少可以優化的地方。
比如,為了實現經典的模式,我們先要定義一個抽象基類,再實現具體的策略類。對於上面這樣一個簡單的計算折扣價格邏輯來說,其實可以用函式來實現,然後在例項化 Order 類時指定這個策略函式即可,大可不必將類給搬出來。這樣就可以避免在下訂單時,不斷的建立策略物件,減少多餘的執行時消耗。這裡就不具體寫出程式碼了。
所以學習設計模式,不僅要知道如何利用這樣的模式組織程式碼,更要領會其思想,活學活用,靈活變通。
以上,就是今天關於 策略模式
的一些個人分享,如有講得不到位的,還請後臺留言指正!
參考文件
- 《流暢的Python》