前言 在一些較為複雜的業務中,客戶端需要依據條件,執行相應的行為或演算法。在實現這些業務時,我們可能會使用較多的分支語句(switch case或if else語句)。使用分支語句,意味著“變化”和“重覆”,每個分支條件都代表一個變化,每個分支邏輯都是相似行為或演算法的重覆。當追加新的條件時,我們需要追... ...
前言
在一些較為複雜的業務中,客戶端需要依據條件,執行相應的行為或演算法。在實現這些業務時,我們可能會使用較多的分支語句(switch case或if else語句)。使用分支語句,意味著“變化”和“重覆”,每個分支條件都代表一個變化,每個分支邏輯都是相似行為或演算法的重覆。
當追加新的條件時,我們需要追加分支語句,並追加相應的行為或演算法。
上一篇文章“使用多態代替條件判斷”中,我們講到它可以處理這些“變化”和“重覆”,今天我將介紹一種新的方式——使用策略模式代替分支,它也能處理這些“變化”和“重覆”。在講這個策略之前,我們先來看一則小故事。
小商城的運營
某小型線上商城,有3位核心成員,他們分別是CTO、COO和CEO。
CTO:小A,負責擼代碼,以及維護商城系統。
COO:小B,負責吹牛忽悠,以及市場推廣和運營。
CEO:小C,負責拉皮條,以及看著你倆幹活。
在這個故事中,假定你就是小A,頭銜CTO(誰讓你既不會拉皮條,也不會吹牛忽悠呢)。
第一幕
某一天,小B策划了一個促銷活動,免費給用戶發放一些優惠券,用戶在消費滿一定金額後,可以使用這些優惠券抵扣。
假定現在有兩個優惠活動——“滿99減20,滿199減50”。
每個用戶要買的東西和花費的金額是不同的,根據不同的消費金額,系統需要判定使用什麼優惠券。
面對這樣一個場景,你說這不是忒簡單了嘛,然後唰唰唰2分鐘就擼完了這串代碼。
public decimal CalculateAmount(decimal amount) { if (amount < 99) return amount; else if (amount < 200) return amount - 20; else return amount - 50; }
小B看了後,說道:“哇,這麼快就弄完了,不愧是咱們公司的CTO,趕緊上線吧!”。
第二幕
第一天,小B根據交易數據分析得知,自從上了優惠券後(我是優惠券,誰要上我?),商城的交易額增長了很多,而且有較多用戶的訂單金額竟然超過了200。
為了回饋這部分“高端”用戶的熱情和貢獻,商城決定加大優惠力度,於是小B追加了兩項優惠活動:滿299減80,滿399減120。(好吧,這和街邊賣場的大叔吆喝是一樣樣的,原價500多的真皮皮鞋、錢包,現在只要50元,全場50元,通通50元…!)
看到這新出現的場景,你想這不是分分鐘搞定的事兒?於是你修改了CalculateAmount()方法。
public decimal CalculateAmount(decimal amount) { if (amount < 99) return amount; else if (amount < 200) return amount - 20; else if(amount < 300) return amount - 50; else if (amount < 400) return amount - 80; else return amount - 120; }
第三幕
第二天,小B又提了一個要求:“有些用戶的會員等級比較高,為了給用戶一種“老子是上帝”的感覺,可以為這些高級會員打一些折扣。”
銅牌會員無折扣,銀牌會員打98折,金牌會員打95折,磚石會員打9折。
這時,你心裡嘀咕了一聲,幹嘛不早說? 改吧,反正也不是啥難事兒。
public decimal CalculateAmount(Customer customer, decimal amount) { // 優惠券減免 if (amount < 99) { } else if (amount <200) amount -= 20; else if(amount <300) amount -= 50; else if (amount < 400) amount -= 80; else amount -= 120; // 會員等級減免 switch (customer.MemberLevel) { case MemberLevel.Silver: amount *= 0.98m; break; case MemberLevel.Gold: amount *= 0.95m; break; case MemberLevel.Diamond: amount *= 0.90m; break; } return amount; }
小B拍了拍你的肩膀,意味深長地說:“網站的維護就全靠你了,咱們會好起來的,賺了錢大家一起分!”。
第四幕
三天之後,小B說這幾天商城銷量非常不錯,咱們應該賺了不少錢。但是用戶現在的激情也降下去了,咱們可以撤回這些優惠了,商品都按原價來賣吧,麻煩你把優惠政策給撤銷吧。
你幽幽地嘆了一口氣:“好吧,現在改(說好的賺錢大家一起分的呢,這茬子事兒你咋不提?一萬匹草泥馬瘋狂地踏過)。”
於是你刪除了調用這個方法的代碼。
第五幕
一個月後,小B又來找你了:“現在又到了購物的旺季,淘寶京東開始做活動了,優惠力度還挺大,咱們也在這股購物潮里湊個熱鬧吧。這一次,我們有以下幾項業務規則,和上次的不同,也比上次的複雜一些,你聽我向你娓娓道來啊。”
1. aaa規則
2. bbb規則
3. ccc規則
4. ddd規則
…
10. xxx規則
聽完這些後,你崩潰了,你這個小B(一語雙關),怎麼一下子提出這麼多業務,還不帶重樣的,我從何改起啊?
設計模式簡介
聽完這個故事後,你能瞭解到什麼呢?我們用兩個詞來概括,也就是本文開頭提到的“變化”和“重覆”。
大多數的“變化”都會伴隨著“重覆”,這些“重覆”的表現形式可能不一樣,但它們的本質是類似的。
無論是生活還是工作,變化是和重覆都是無處不在的。
在工作中,我們處於某一個崗位,我們每天的工作任務都會有變化,我們使用近乎相同的方式處理這些工作任務。
代碼中也會出現很多“變化”和“重覆”,我們該如何應對呢?。
你可以借用一些設計模式,怎麼用設計模式咱先不說,我們先粗略地解一下設計模式。
設計模式我們把它拆分成兩個來看,“設計”和“模式”。
“設計”就是設計,對於軟體系統來說,即分析問題並解決問題的過程。
“模式”是指事物的標準。在軟體領域,每個人面臨的問題是不同的,雖然不同的問題有不同的標準,但很多問題本質上是類似的。
設計模式更多的是軟體層面的,而非業務層面的。
即使你用了設計模式,你也不一定能解決業務上的問題。
即使你不用設計模式,業務上的問題你也許能通過其他途徑解決。
設計模式怎麼用,在這裡我也無法給一個確定的答案。
我個人的看法是,儘量做到“心中無模式”。最關鍵的是,你應該直面問題的本質,尋找問題最有效的解決方式,不要一遇到問題,就誇誇其談地使用某某設計模式。真切地從用戶角度去出發,去剖析問題的本質,並提出合理的設計和解決方案。當問題得以解決,你回顧這個過程時,你會發現很多模式你是自然而然地用到了。不要特別在意設計模式,這可能會讓你忽視問題的本質,即使你把GOF的23種設計模式倒背如流,你解決問題的能力也不會有所提升。
PS:為了描述“變化”和“重覆”,我使用了小商城運營這個故事。這個故事裡面有些不恰當的地方,搞線上購物的是不會這麼去設計優惠券和折扣的。
策略模式
正式進入今天的主題吧,這篇文章要提到的設計模式是“策略模式”。
定義
策略模式是設計模式裡面較為簡單的一種,它的定義如下:
The Strategy Pattern defines a family of algorithms,encapsulates each one,and makes them interchangeable. Strategy lets the algorithm vary independently from clients that use it.策略模式定義了一系列的演算法,並將每一個演算法封裝起來,而且使它們還可以相互替換。
在策略模式中,演算法是其中的“變化點”,策略模式讓演算法獨立於使用它的客戶而獨立變化。
組成部分
策略模式有4個部分組成:
1. 客戶端:指定使用哪種策略,它依賴於環境
2. 環境:依賴於演算法介面,併為客戶端提供演算法介面的實例
3. 抽象策略:定義公共的演算法介面
4. 策略實現:演算法介面的具體實現
下麵這幅圖詮釋了這4個組成部分:
註意:在策略模式中,策略是由用戶選擇的,這意味著具體策略可能都要暴露給客戶端,但是我們可以通過“分解依賴”來隱藏策略細節。
示例
重構前
該示例是一家物流公司根據State計算物流運費的場景,ShippingInfo類的CalculateShippingAmount()方法,會按照不同的State計算出運輸費用。物流公司最開始只處理3個State的運輸業務,分別是Alaska, NewYork和Florida。
隱藏代碼public class ClientCode { public decimal CalculateShipping() { ShippingInfo shippingInfo = new ShippingInfo(); return shippingInfo.CalculateShippingAmount(State.Alaska); } } public enum State { Alaska, NewYork, Florida } public class ShippingInfo { public decimal CalculateShippingAmount(State shipToState) { switch (shipToState) { case State.Alaska: return GetAlaskaShippingAmount(); case State.NewYork: return GetNewYorkShippingAmount(); case State.Florida: return GetFloridaShippingAmount(); default: return 0m; } } private decimal GetAlaskaShippingAmount() { return 15m; } private decimal GetNewYorkShippingAmount() { return 10m; } private decimal GetFloridaShippingAmount() { return 3m; } }
這段代碼使用了switch case分支語句,每個State都有相應的運費演算法。當物流公司業務擴大,追加新的State時,我們不得不追加switch case分支,並提供新的State的運費演算法。
在不遠的將來,ShippingInfo類將變成這樣:
- CalculateShippingAmount()方法中包含了大量的switch case分支
- 大量的運費演算法使得ShippingInfo變得非常臃腫
從職責角度看,運費演算法是另外一個層面的職責,我們也理應將運費演算法從ShippingInfo中剝離出來。
重構後
為了演示策略模式的各個組成部分,我將重構後的代碼拆分為4個部分,下圖是重構後的UML圖示。
抽象策略
計算運費的策略介面,在介面中定義了State屬性和Calculate()計算方法。
public interface IShippingCalculation { State State { get; } decimal Calculate(); }
策略實現
計算運費的策略實現,分別實現了Alask、NewYork和Florida三個州的運算策略。
public class AlaskShippingCalculation : IShippingCalculation { public State State { get { return State.Alaska; } } public decimal Calculate() { return 15m; } } public class NewYorkShippingCalculation : IShippingCalculation { public State State { get { return State.NewYork; } } public decimal Calculate() { return 10m; } } public class FloridaShippingCalculation : IShippingCalculation { public State State { get { return State.Florida; } } public decimal Calculate() { return 3m; } }
環境
IShippingInfo介面相當於環境介面,ShippingInfo相當於環境具體實現,ShippingInfo知道所有的運算策略。
public interface IShippingInfo { decimal CalculateShippingAmount(State state); } public class ShippingInfo : IShippingInfo { private IDictionary<State, IShippingCalculation> ShippingCalculations { get; set; } public ShippingInfo(IEnumerable<IShippingCalculation> shippingCalculations) { ShippingCalculations = shippingCalculations.ToDictionary(calc => calc.State); } public decimal CalculateShippingAmount(State state) { return ShippingCalculations[state].Calculate(); } }
客戶端
ClientCode表示客戶端,由客戶端指定運輸目的地,它通過IShippingInfo獲取運費計算結果。
客戶端依賴於IShippingInfo介面,這使運費計算策略得以隱藏,並解除了客戶端對具體環境的依賴性。
public class ClientCode { public IShippingInfo ShippingInfo { get; set; } public decimal CalculateShipping() { return ShippingInfo.CalculateShippingAmount(State.Alaska); } }
使用分支還是策略模式?
通過上面這個示例,大家可以清晰地看到,重構後的代碼比重構前複雜的多。出現新的State時,雖然我們可以方便地擴展新的策略,但是會導致策略類越來越多,這意味著我們可能需要維護大量的策略類。
有些人會覺得重構前的代碼會比較實用,雖然耦合性高,無擴展性,但代碼也比較好改——想使用哪種方式完全取決於你。
(在實際的應用中,運費計算遠比示例中的代碼要複雜的多。比如:需要依據當前的油價、運輸路線、運輸工具、運輸時間等各種條件來計算。)
另外,我們不應該一遇到分支語句,就想著把它改造成策略模式,這是設計模式的濫用。
如果分支條件是比較固定的,而且每個分支處理邏輯較為簡單,我們就沒必要使用設計模式。
總的來說,使用分支判斷還是策略模式?答案是:It depends on you.
【關註】keepfool