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

来源:http://www.cnblogs.com/keepfool/archive/2016/05/17/5503516.html
-Advertisement-
Play Games

前言 在一些較為複雜的業務中,客戶端需要依據條件,執行相應的行為或演算法。在實現這些業務時,我們可能會使用較多的分支語句(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個組成部分:

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.

【關註】keepfool
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 對於每個Java程式員來說,HelloWorld是一個再熟悉不過的程式。它很簡單,但是這段簡單的代碼能指引我們去深入理解一些複雜的概念。這篇文章,我將探索我們能從這段簡單的代碼中學到什麼。如果你對HelloWorld有獨到的理解,請留下你的評論。 HelloWorld.java 為什麼所有東西都是從 ...
  • 前言 Verilog是一種硬體描述語言(HDL),該語言在Windows上有集成開發環境可以使用,如ModelSim,但ModelSim的編輯器不太好用因此筆者萌生了用Sublime Text3來編寫Verilog的想法。下麵我們將圍繞著Sublime Text3搭建起一個簡易的IDE, 我將儘量把 ...
  • 一: 提供服務的遠程一端 1-1. applicationContext.xml 1-2. 介面 1-3. javabean 1-4. 實現類 1-5. ServerTest類 二: 本地調用一端 2-1. applicationContext-client 2-2. ClientTest類 ...
  • 前段時間,team使用了七牛鏡像的功能,用到了,就決定瞭解一下。 七牛官網的說明如下: 設置鏡像存儲,源站資源(文件/圖片等)根據初次訪問自動同步到七牛雲存儲,數據平滑遷移。可使用綁定的自定義功能變數名稱訪問鏡像存儲的源站資源。 配置鏡像存儲後,因為鏡像源和鏡像空間內容基本一致,將可能導致搜索引擎對源站進行 ...
  • 簡介 以前在使用Hibernate的時候知道其有一級緩存和二級緩存,限制ORM框架的發展都是互相吸收其他框架的優點,在Hibernate中也有一級緩存和二級緩存,用於減輕數據壓力,提高資料庫性能。 mybaits提供一級緩存和二級緩存結構如下圖: 可以看出一級緩存是sqlSession級別的,而二級 ...
  • 技術介紹 devtools:是boot的一個熱部署工具,當我們修改了classpath下的文件(包括類文件、屬性文件、頁面等)時,會重新啟動應用(由於其採用的雙類載入器機制,這個啟動會非常快,如果發現這個啟動比較慢,可以選擇使用jrebel) 雙類載入器機制:boot使用了兩個類載入器來實現重啟(r ...
  • 下列方法僅提供 Windows 平臺使用,所以需要使用編譯開關,代碼如下: ...
  • 常練習即可很好的應用和記住NumberFormat類的使用。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...