小酌重構系列[15]——策略模式代替分支

前言

在一些較爲複雜的業務中,客戶端須要依據條件,執行相應的行爲或算法。在實現這些業務時,咱們可能會使用較多的分支語句(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:爲了描述「變化」和「重複」,我使用了小商城運營這個故事。這個故事裏面有些不恰當的地方,搞在線購物的是不會這麼去設計優惠券和折扣的。

策略模式

正式進入今天的主題吧,這篇文章要提到的設計模式是「策略模式」。

定義

策略模式是設計模式裏面較爲簡單的一種,它的定義以下:

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個組成部分:

image

注意:在策略模式中,策略是由用戶選擇的,這意味着具體策略可能都要暴露給客戶端,可是咱們能夠經過「分解依賴」來隱藏策略細節。

示例

重構前

該示例是一家物流公司根據State計算物流運費的場景,ShippingInfo類的CalculateShippingAmount()方法,會按照不一樣的State計算出運輸費用。物流公司最開始只處理3個State的運輸業務,分別是Alaska, NewYork和Florida。

image

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圖示。

image

抽象策略

計算運費的策略接口,在接口中定義了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.

相關文章
相關標籤/搜索