github地址: https://github.com/cheesezh/python_design_patterns
題目
設計一個控制台程序, 模擬商場收銀軟件,根據客戶購買商品的單價和數量,計算總價。
基礎版本
price = float(input("輸入商品單價:"))
number = int(input("輸入商品數量:"))
total = (price * number)
print("當前總價: %.2f" % total)
輸入商品單價:40
輸入商品數量:9
當前總價: 360.00
點評
上述程序僅僅實現了基本功能,但是當商場有打折活動,例如八折,五折等,就不滿足需求了,折扣的方法還可能有滿減活動,例如滿300減100,滿500減200等。假設只有打折和滿減兩種促銷活動,那么這就很像上一章節的計算器,支持正常收費,打折活動和滿減活動三種計算方法,可以用簡單工廠方法實現。
改進版本1.0——簡單工廠模式
from abc import ABCMeta, abstractmethod
class CashBase():
"""
基礎類
"""
__metaclass__ = ABCMeta
def __init__(self):
self.final_price = None
@abstractmethod
def accept_cash(self):
pass
class CashNormal(CashBase):
"""
正常收費
"""
def accept_cash(self, money):
self.final_price = money
return self.final_price
class CashRebate(CashBase):
"""
打折活動
"""
def __init__(self, rebate):
self.rebate = rebate
def accept_cash(self, money):
self.final_price = money * self.rebate
return self.final_price
class CashReturn(CashBase):
"""
滿減活動
"""
def __init__(self, return_condition, return_money):
self.return_condition = return_condition
self.return_money = return_money
def accept_cash(self, money):
if money >= self.return_condition:
self.final_price = money - self.return_money
else:
self.final_price = money
return self.final_price
class CashFactory():
"""
收費方式工廠類
"""
# 類的變量,類似靜態變量,通過`類名.變量名`訪問
cash_accepter_map = {
"正常收費": CashNormal(),
"滿300減100": CashReturn(300, 100),
"打8折": CashRebate(0.8)
}
@staticmethod
def createCashAccepter(cash_type):
if cash_type in CashFactory.cash_accepter_map:
return CashFactory.cash_accepter_map[cash_type]
else:
return None
客戶端代碼
price = float(input("輸入商品單價:"))
number = int(input("輸入商品數量:"))
cash_type_list = ["正常收費", "滿300減100", "打8折"]
for i in cash_type_list:
print("{}:{}".format(cash_type_list.index(i)+1, i))
cash_type_index = int(input("選擇收費方式(1~3)"))
total = price * number
cash_accepter = CashFactory.createCashAccepter(cash_type_list[cash_type_index-1])
print("應收: %.2f" % total)
total = cash_accepter.accept_cash(total)
print("實收: %.2f" % total)
輸入商品單價:10
輸入商品數量:50
1:正常收費
2:滿300減100
3:打8折
選擇收費方式(1~3)3
應收: 500.00
實收: 400.00
點評
- 如果同時支持打折和滿減,需要如何處理?
- 簡單工廠模式主要解決對象的創建問題,無法解決對象經常改動的問題,例如折扣和滿減力度是經常變化的,不能每次改動都改代碼;
- 算法經常改動, 需要用到策略模式;
- 封裝變化點是面向對象的一種重要的思維方式。
策略模式
該模式定義了算法家族,分別封裝起來,讓它們之間可以互相替換,此模式讓算法的變化,不會影響到使用算法的客戶。
from abc import ABCMeta, abstractmethod
class CashBase():
"""
抽象策略:基礎類
"""
__metaclass__ = ABCMeta
def __init__(self):
self.final_price = None
@abstractmethod
def accept_cash(self):
pass
class CashNormal(CashBase):
"""
具體策略:正常收費
"""
def accept_cash(self, money):
self.final_price = money
return self.final_price
class CashRebate(CashBase):
"""
具體策略:打折活動
"""
def __init__(self, rebate):
self.rebate = rebate
def accept_cash(self, money):
self.final_price = money * self.rebate
return self.final_price
class CashReturn(CashBase):
"""
具體策略:滿減活動
"""
def __init__(self, return_condition, return_money):
self.return_condition = return_condition
self.return_money = return_money
def accept_cash(self, money):
if money >= self.return_condition:
self.final_price = money - self.return_money
else:
self.final_price = money
return self.final_price
class CashContext():
"""
策略上下文類(基礎版本),用具體策略類來配置,維護一個具體策略對象的引用
"""
def __init__(self, cash_strategy):
self.cash_strategy = cash_strategy
def get_result(slef, money):
return self.cash_strategy.accept_cash(money)
點評
在CashContext類中,我們需要傳入一個具體策略類來進行配置,在商場收銀軟件這個場景中,那就是不同的收費策略,那么如何生成不同的收費策略對象呢?可以將策略模式和簡單工廠相結合。
class CashContext():
"""
策略上下文類(改進版本),用具體策略類來配置,維護一個具體策略對象的引用
"""
# 類的變量,類似靜態變量,通過`類名.變量名`訪問
cash_accepter_map = {
"正常收費": CashNormal(),
"滿300減100": CashReturn(300, 100),
"打8折": CashRebate(0.8)
}
def __init__(self, cash_type):
self.cash_strategy = CashContext.cash_accepter_map[cash_type]
def get_result(self, money):
return self.cash_strategy.accept_cash(money)
客戶端代碼
price = float(input("輸入商品單價:"))
number = int(input("輸入商品數量:"))
cash_type_list = ["正常收費", "滿300減100", "打8折"]
for i in cash_type_list:
print("{}:{}".format(cash_type_list.index(i)+1, i))
cash_type_index = int(input("選擇收費方式(1~3)"))
total = price * number
cash_context = CashContext(cash_type_list[cash_type_index-1])
print("應收: %.2f" % total)
total = cash_context.get_result(total)
print("實收: %.2f" % total)
輸入商品單價:10
輸入商品數量:10
1:正常收費
2:滿300減100
3:打8折
選擇收費方式(1~3)3
應收: 100.00
實收: 80.00
點評
策略模式+簡單工廠和僅用簡單工廠模式的區別在哪里呢?
簡單工廠
cash_accepter = CashFactory.createCashAccepter(cash_type_list[cash_type_index-1])
...
total = cash_accepter.accept_cash(total)
策略模式+簡單工廠
cash_context = CashContext(cash_type_list[cash_type_index-1])
...
total = cash_context.get_result(total)
- 簡單工廠需要讓客戶端認識兩個類,
CashFactory
和CashBase
- 策略模式+簡單工廠,客戶端只需要認識一個類,
CashContext
- 客戶端實例化的是
CashContext
的對象,調用的是CashContext
的get_result
方法,這使得具體的收費策略徹底與客戶端分離,甚至連策略的基類CashBase
都不需要客戶端認識。
策略模式解析
- 策略模式是一種定義一系列算法的方法,從概念上來看,所有這些算法完成的都是相同的工作,只是實現不同,它可以以相同的方式調用素有的算法,減少了各種算法類與使用算法類之間的耦合[DPE]。
- 策略模式的Strategy層次為Context定義了一系列的可供重用的算法或行為。繼承有助於析取出這些算法中的公共功能[DP],例如計算費用的結果get_result。
- 策略模式可以簡化單元測試,因為每個算法都有自己的類,可以用過自己的接口單獨測試[DPE]。
- 策略模式是用來封裝算法的,但是實踐中,可以用它來封裝幾乎任何類型的規則,只要需要不同時間應用不同業務規則,就可以考慮使用策略模式處理這種變化的可能性[DPE]。
美中不足
在CashContext中用到了一個dict()型的類的變量cash_accepter_map
保存各種算法策略,如果新增滿200減50
的策略,那么還要更新cash_accepter_map
,這顯得並不優雅,任何需要的變更都需要成本,但是成本的高低是有差異的
,為了更加優雅,降低變更成本,可以使用反射技術
,這一技術將在抽象工廠模式
中介紹。