在一些較爲複雜的業務中,客戶端須要依據條件,執行相應的行爲或算法。在實現這些業務時,咱們可能會使用較多的分支語句(switch case或if else語句)。使用分支語句,意味着「變化」和「重複」,每一個分支條件都表明一個變化,每一個分支邏輯都是類似行爲或算法的重複。
當追加新的條件時,咱們須要追加分支語句,並追加相應的行爲或算法。html
上一篇文章「使用多態代替條件判斷」中,咱們講到它能夠處理這些「變化」和「重複」,今天我將介紹一種新的方式——使用策略模式代替分支,它也能處理這些「變化」和「重複」。在講這個策略以前,咱們先來看一則小故事。算法
某小型在線商城,有3位核心成員,他們分別是CTO、COO和CEO。
CTO:小A,負責擼代碼,以及維護商城系統。
COO:小B,負責吹牛忽悠,以及市場推廣和運營。
CEO:小C,負責拉皮條,以及看着你倆幹活。設計模式
在這個故事中,假定你就是小A,頭銜CTO(誰讓你既不會拉皮條,也不會吹牛忽悠呢)。工具
某一天,小B策劃了一個促銷活動,免費給用戶發放一些優惠券,用戶在消費滿必定金額後,可使用這些優惠券抵扣。
假定如今有兩個優惠活動——「滿99減20,滿199減50」。網站
每一個用戶要買的東西和花費的金額是不一樣的,根據不一樣的消費金額,系統須要斷定使用什麼優惠券。
面對這樣一個場景,你說這不是忒簡單了嘛,而後唰唰唰2分鐘就擼完了這串代碼。spa
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元…!)code
看到這新出現的場景,你想這不是分分鐘搞定的事兒?因而你修改了CalculateAmount()方法。htm
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又提了一個要求:「有些用戶的會員等級比較高,爲了給用戶一種「老子是上帝」的感受,能夠爲這些高級會員打一些折扣。」blog
銅牌會員無折扣,銀牌會員打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:爲了描述「變化」和「重複」,我使用了小商城運營這個故事。這個故事裏面有些不恰當的地方,搞在線購物的是不會這麼去設計優惠券和折扣的。
正式進入今天的主題吧,這篇文章要提到的設計模式是「策略模式」。
策略模式是設計模式裏面較爲簡單的一種,它的定義以下:
策略模式定義了一系列的算法,並將每個算法封裝起來,並且使它們還能夠相互替換。
在策略模式中,算法是其中的「變化點」,策略模式讓算法獨立於使用它的客戶而獨立變化。
策略模式有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類將變成這樣:
從職責角度看,運費算法是另一個層面的職責,咱們也理應將運費算法從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.