新博客 "wossoneri.com" " 設計模式系列目錄 " 需求情景 比如現在需要做一個收銀軟體,要根據用戶所買商品的單價和數量進行計算。 很簡單,用“單價 數量”即可。 但如果某天需要打折呢? 也很簡單,同一個方法,把折扣作為一個參數,預設值為1,代碼改為“單價 數量 折扣”即可。 恩,看起 ...
需求情景
比如現在需要做一個收銀軟體,要根據用戶所買商品的單價和數量進行計算。
很簡單,用“單價 * 數量”即可。
但如果某天需要打折呢?
也很簡單,同一個方法,把折扣作為一個參數,預設值為1,代碼改為“單價 * 數量 * 折扣”即可。
恩,看起來都很美好。現在又要加需求,我要滿300減100,我還要滿200送50...
OK,現在就得回到面向對象上來了。向上次簡單工廠一樣,把所有計算價格可能的方法封裝成一個個類。比如一個打一折類,一個打兩折類...唉等等,這可不對。上次加、減、乘、除分別封裝是因為他們屬於同一種類型,但是有不同的實現方法。而這次,對於打折來說,不論打幾折,打折的計算方式都是一樣的,只是形式不同,但本質是一樣的。同理,滿減和返利也是兩種類型,但各自有多種實現。
面向對象的編程,並不是類越多越好,類的劃分是為了封裝,但分類的基礎是抽象,具有相同屬性和功能的對象的抽象集合才是類
所以可以開始編碼,先抽象一個計算收款的類,抽象一個收錢的方法,然後根據不同打折類型實現不同的收錢方法。
@interface Cash : NSObject
- (CGFloat)acceptOriginCash: (CGFloat)money;
@end
@implementation Cash
- (CGFloat)acceptOriginCash: (CGFloat)money {
return money;
}
@end
///正常價錢
@implementation CashNormal
- (CGFloat)acceptOriginCash:(CGFloat)money {
return money;
}
@end
///折扣
@interface CashRebate : Cash
@property (nonatomic, assign) CGFloat rebate;
@end
@implementation CashRebate
- (instancetype)init {
self = [super init];
if (self) {
_rebate = 1.0; //預設不打折
}
return self;
}
- (CGFloat)acceptOriginCash:(CGFloat)money {
return money * _rebate;
}
@end
///滿返
@interface CashReturn : Cash
@property (nonatomic, assign) CGFloat moneyCondition;
@property (nonatomic, assign) CGFloat moneyReturn;
@end
@implementation CashReturn
- (instancetype)init {
self = [super init];
if (self) {
_moneyReturn = 0;
_moneyCondition = 0;
}
return self;
}
- (CGFloat)acceptOriginCash:(CGFloat)money {
if (_moneyCondition == 0 || _moneyReturn == 0 || money < _moneyCondition) {
return money; //沒有返現
} else {
int returnCount = floorf(money / _moneyCondition);
money -= returnCount * _moneyReturn;
return money;
}
}
@end
創建好以上幾種收費類型,設想一下,一般打折時都會列出相應的打折商品,也就是說平時不是所有的商品都打折,這時候假設我們專門寫好一個折扣日的類,類中包含了打折商品列表,當然也包含了打折的方式等其他信息。繼續用面向對象的思想去思考,折扣日應該也分好幾種,比如周末,五一,工作日等等,所以折扣日也可以抽象一個基類出來,這個基類就應該包含返回折扣結果的抽象方法。
OK,到這裡問題就來了,不同的折扣日都有相同的獲取最終價錢的方法,而對於價錢的計算策略卻完全不同,也就是每個具體的折扣日實現這個返回折扣結果的抽象方法都不一樣。那該怎麼做?
設計原則:找到系統中變化的部分,將變化的部分同其它穩定的部分隔開。換句話說就是:"找到變化並且把它封裝起來,稍後你就可以在不影響其它部分的情況下修改或擴展被封裝的變化部分。"儘管這個概念很簡單,但是它幾乎是所有設計模式的基礎,所有模式都提供了使系統里變化的部分獨立於其它部分的方法。
可以看出,每個折扣日都要實現基類返回折扣結果的方法,但實現的方法不一樣。而計算方法都是經過了封裝的,保證計算方法不被改變,也保證改變一個不會影響到其他計算方法。在這種情況下,就可以考慮使用策略模式。
策略模式
策略模式定義了演算法家族,分別封裝起來,讓他們之間可以互相替換,此模式讓演算法的變化不會影響到使用演算法的客戶。
以上幾種收錢方式都是一些演算法,演算法本身只是一種策略,最重要的是這些演算法是隨時都可能且可以互相替換的,這就是變化點,而封裝變化點是我們面向對象很重要的思維方式。
所以這裡的思路是創建一個上下文類,用策略對象作為構造參數,來維護一個對策略對象的引用。同時這樣也不會受到拓展的影響。
@interface CashContext : NSObject
- (instancetype)initWithCash: (Cash *)cash;
- (CGFloat)getResult: (CGFloat)money;
@end
///
@interface CashContext()
{
Cash *_cash;
}
@end
@implementation CashContext
- (instancetype)initWithCash: (Cash *)cash {
self = [super init];
if (self) {
_cash = cash;
}
return self;
}
- (CGFloat)getResult: (CGFloat)money {
return [_cash acceptOriginCash:money];
}
@end
創建好Context類,就可以通過構造方法選擇不同的策略來實現計算:
CashContext *context = [[CashContext alloc] initWithCash:[[CashRebate alloc] initWithRebate:0.8]];//打8折
CGFloat value = [context getResult:400]]//原價400
UML類圖
應用場景和優缺點
應用
- 多個類只區別在表現行為不同,可以使用Strategy模式,在運行時動態選擇具體要執行的行為。
- 需要在不同情況下使用不同的策略(演算法),或者策略還可能在未來用其它方式來實現。
- 對客戶隱藏具體策略(演算法)的實現細節,彼此完全獨立。(你只要知道Context類的介面,不必知道折扣演算法內部是怎麼實現的)
優點
- 提供了一種替代繼承的方法,而且既保持了繼承的優點(代碼重用)還比繼承更靈活(演算法獨立,可以任意擴展)。
- 避免程式中使用多重條件轉移語句,使系統更靈活,並易於擴展。
- 遵守大部分GRASP原則和常用設計原則,高內聚、低偶合。
缺點
- 因為每個具體策略類都會產生一個新類,所以會增加系統需要維護的類的數量。
參考
鴨子-策略模式(Strategy)
這篇文章更深入形象,推薦閱讀