看圖識模式
今天從收銀台的例子說起(不要嫌棄~)
商場需要一套收銀台系統,這套系統除了基本的計算商品價格增減外,還要應付各種節假日的促銷活動:
相信即使一個程式設計的新手也一樣能勝任這套系統的開發,實在沒有太多難度~
可如果想讓它的代碼設計合理、易于拓展、利于維護、便于測試。這可就有難度了。
高手和新手的差别除了知識面廣、專業度深之外就是代碼的設計能力。其實絕大部分的任務新手也能完全搞定,無非就是多改改bug,多加加班而已。但是新手價格經濟實惠,不挑剔,不怕被坑,多好,咳咳...跑題了
方案一:
上面的每種售價方案其實對應一種算法,把三個算法寫在調用類中,通過if-else判斷類型調用對應的算法生成價格即可。
這是對簡單的寫法,談不上設計,但是也完成了功能。
不足之處是: 用戶端調用的地方備援了大量的邏輯,導緻系統臃腫,難以維護且無法
拓展,算法也無法複用。這肯定是不行的
方案二:
其實三個算法是相同的功能,隻是執行了不同的政策,我們可以把這三個算法封裝到一個生成價格類中,用戶端隻需要傳遞一個類型,調用這個類生成價格即可。生成價格類通過類型判斷調用對應的算法。
價格計算邏輯和調用端分離解耦。但是還有一些其它問題: 如果新增促銷政策還
需要增加生成價格類中的if-else類型判斷,而且添加一個新算法。這不符合
開放-封閉原則。而且算法隻被價格計算類使用,也不能達到複用的效果。
第二個方案中解決了價格計算邏輯和調用端之間的耦合問題,剩下的問題改如何解決呢?其實當我們看到這種大量堆砌的if-else的語句時就讀到了一種壞壞的味道。
下面介紹的這種設計模式有利于我們解決這種問題。
模式定義
定義
政策模式(Strategy Pattern):定義一系列算法,把它們一個一個封裝起來,并且使它們可以互相替換。政策模式讓算法可獨立于使用它的客戶而變化。
結構圖
責任鍊模式包含如下角色:
- 環境類(Context): 維護一個對Strategy對象的引用,可避免高層子產品對政策的直接調用。
- 抽象政策類(Strategy): 定義所有支援的算法的公共接口。
- 具體政策類(ConcreteStrategy): 以Strategy接口實作某具體算法。
在政策模式中,調用算法的主體封裝到了封裝類Context中,抽象政策Strategy一般是一個接口,目的隻是為了定義規範,裡面一般不包含邏輯。
代碼
通過上面的定義,下面來設計一下代碼實作:
價格類對應的是context,context類引用了一個具體的價格算法(政策),代碼如下:
CashScheme價格計算方法抽象類,對應抽象政策類
@interface CashScheme : NSObject
- (double)acceptCash:(double)number;
@end
@implementation CashScheme
- (double)acceptCash:(double)number
{
return 0;
}
@end
CashRebate折扣計算方法類,對應具體政策類
@interface CashRebate : CashScheme
- (instancetype)initWithMoneyRebate:(float)moneyRebate;
@end
@interface CashRebate ()
@property (nonatomic, assign) float moneyRebate;
@end
@implementation CashRebate
- (instancetype)initWithMoneyRebate:(float)moneyRebate
{
self = [super init];
if (self) {
_moneyRebate = moneyRebate;
}
return self;
}
- (double)acceptCash:(double)number
{
return (self.moneyRebate * number);
}
@end
CashReturn返現計算方法類,對應具體政策類
@interface CashReturn : CashScheme
- (instancetype)initWithCondition:(double)condition returnCount:(double)count;
@end
@interface CashReturn ()
@property (nonatomic, assign) double condition; // 條件值(達到多少可以參加返現)
@property (nonatomic, assign) double returnCount; // 返現值
@end
@implementation CashReturn
- (instancetype)initWithCondition:(double)condition returnCount:(double)count;
{
self = [super init];
if (self) {
_condition = condition;
_returnCount = count;
}
return self;
}
- (double)acceptCash:(double)number
{
double result = number;
if (number >= self.condition) {
result = number - self.returnCount;
}
return result;
}
@end
CashNormal無優惠(日常)計算方法類,對應具體政策類
@interface CashNormal : CashScheme
@end
@implementation CashNormal
- (double)acceptCash:(double)number
{
return number;
}
@end
CashContext生成價格類,對應context
@interface CashContext : NSObject
@property (nonatomic, strong) CashScheme *cashScheme;
- (double)getResult:(double)number;
@end
@implementation CashContext
- (double)getResult:(double)number
{
double result = number;
if (self.cashScheme) {
result = [self.cashScheme acceptCash:number];
}
return result;
}
@end
用戶端調用
CashContext *cashContext = [[CashContext alloc] init];
CashScheme *cashScheme = nil;
int type = 3;
switch (type) {
case 1:
{
cashScheme = [[CashNormal alloc] init];
}
break;
case 2:
{
cashScheme = [[CashRebate alloc] initWithMoneyRebate:0.5];
}
break;
case 3:
{
cashScheme = [[CashReturn alloc] initWithCondition:1000 returnCount:200];
}
break;
default:
cashScheme = [[CashNormal alloc] init];
break;
}
cashContext.cashScheme = cashScheme;
int result = [cashContext getResult:1000];
NSLog(@"目前活動後的價格是: %@", @(result));
結果
現在這樣的設計很好的解決了我上面提出的問題,增加新的促銷活動時隻需新增一個繼承至CashScheme的類,在acceptCash方法裡實作自己的政策,而不用修改原有的系統,也不會影響以前的算法政策。用戶端指定對應的算法政策給context就完成了對新算法的調用。這完全符合開放-封閉原則。
由于每個算法都有自己單獨的類,簡化了單元測試。而且算法也可以被複用。
用戶端調用中的type是由運作時外部條件決定的,我這裡假設為3。這也是這個模式的一個缺點,用戶端需要知道不同算法政策的差別,在不同的條件下組織不同的算法政策。
這可缺點可以通過和簡單工廠模式相結合來解決,這個大家自己思考一下!由于簡單工廠模式很簡單,不清楚的可以抽一點點時間看一下~
特征
動機
- 完成一項任務,往往可以有多種不同的方式,每一種方式稱為一個政策,我們可以根據環境或者條件的不同選擇不同的政策來完成該項任務。
- 在軟體開發中也常常遇到類似的情況,實作某一個功能有多個途徑,此時可以使用一種設計模式來使得系統可以靈活地選擇解決途徑,也能夠友善地增加新的解決途徑。
- 在軟體系統中,有許多算法可以實作某一功能,如查找、排序等,一種常用的方法是寫死(Hard Coding)在一個類中,如需要提供多種查找算法,可以将這些算法寫到一個類中,在該類中提供多個方法,每一個方法對應一個具體的查找算法;當然也可以将這些查找算法封裝在一個統一的方法中,通過if…else…等條件判斷語句來進行選擇。這兩種實作方法我們都可以稱之為寫死,如果需要增加一種新的查找算法,需要修改封裝算法類的源代碼;更換查找算法,也需要修改用戶端調用代碼。在這個算法類中封裝了大量查找算法,該類代碼将較複雜,維護較為困難。
- 除了提供專門的查找算法類之外,還可以在用戶端程式中直接包含算法代碼,這種做法更不可取,将導緻用戶端程式龐大而且難以維護,如果存在大量可供選擇的算法時問題将變得更加嚴重。
- 為了解決這些問題,可以定義一些獨立的類來封裝不同的算法,每一個類封裝一個具體的算法,在這裡,每一個封裝算法的類我們都可以稱之為政策(Strategy),為了保證這些政策的一緻性,一般會用一個抽象的政策類來做算法的定義,而具體每種算法則對應于一個具體政策類。
使用場景
在以下情況下可以使用政策模式:
- 如果在一個系統裡面有許多類,它們之間的差別僅在于它們的行為,那麼使用政策模式可以動态地讓一個對象在許多行為中選擇一種行為。
- 一個系統需要動态地在幾種算法中選擇一種。
- 如果一個對象有很多的行為,如果不用恰當的模式,這些行為就隻好使用多重的條件選擇語句來實作。
- 不希望用戶端知道複雜的、與算法相關的資料結構,在具體政策類中封裝算法和相關的資料結構,提高算法的保密性與安全性。
優缺點
優點
政策模式的優點
- 政策模式提供了對“開閉原則”的完美支援,使用者可以在不修改原有系統的基礎上選擇算法或行為,也可以靈活地增加新的算法或行為。
- 政策模式提供了管理相關的算法族的辦法。
- 政策模式提供了可以替換繼承關系的辦法。
繼承提供了另一種支援多種算法或行為的方法。你可以直接生成一個Context類的子 類,進而給它以不同的行為。但這會将行為硬行編制到 Context中,而将算法的實 現與Context的實作混合起來,進而使Context難以了解、難以維護和難以擴充,而 且還不能動态地改變算法。最後你得到一堆相關的類 , 它們之間的唯一差别是它們 所使用的算法或行為。 将算法封裝在獨立的Strategy類中使得你可以獨立于其 Context改變它,使它易于切換、易于了解、易于擴充。
- 使用政策模式可以避免使用多重條件轉移語句。
Strategy模式提供了用條件語句選擇所需的行為以外的另一種選擇。當不同的行為堆 砌在一個類中時 ,很難避免使用條件語句來選擇合适的行為。将行為封裝在一個個獨 立的Strategy類中消除了這些條件語句。含有許多條件語句的代碼通常意味着需要使 用Strategy模式。
缺點
政策模式的缺點
- 用戶端必須知道所有的政策類,并自行決定使用哪一個政策類(可以通過簡單工廠模式來彌補)。
- 政策模式将造成産生很多政策類,可以通過使用享元模式在一定程度上減少對象的數量。