只有累積,沒有奇蹟

2019年9月18日 星期三

[C#] Refactor - 重構 Switch case 陳述句

前言
在開發程式時使用判斷式是很常見的事情,簡單的判斷式可以使用 if 達到目的,但遇到多個條件或是三個以上時就可以使用 switch case 來解決此問題,但隨著需求的新增團隊可能在 switch case 的每個判斷句又加了很多新代碼,(最近在維護公司的專案上很常見到感觸很深),舉例來說可能代碼會長得像下面這樣
這篇就針對此問題重構方式做簡單介紹,若有更好的方式歡迎隨時提出來一起討論。

重構
從上述代碼可以感受到會有程式碼的壞味道,舉例來說可能存在以下問題
  • 閱讀上會較難閱讀
  • 加 case 時候時需要在這段 code 繼續增加情境來滿足需求,EX : 新增黑豹,這段代碼又要多好幾行
  • 如果隱藏各自 case 所需要做的事情,代碼描述上會更為明確
  • 要針對 switch case 重構有很多種方法,像是 Strategy pattern、Visitor pattern,但自己遇到這問題時還是最推薦使用 Dictionary 方式來進行重構,語意較為清楚團隊在看代碼時也不會太難懂,簡單說明步驟如下

    定義 Enum
    在 switch case 看到在進行判斷時是使用 string 作為依據,但在可能會存在像是判斷大小寫、打錯字、去空白等一些小問題,因此自己在遇到此問題的第一步會先將 switch 需要判斷的項目抽成 Enum,這樣在後續會更方便使用,也不會有打錯字的問題發生
    public enum Hero
    {
        Thanos,
        CaptainAmerica,
        IronMan,
        Thor,
        DoctorStrange,
        Hulk,
        SpiderMan
    }

    定義 Lookup
    定義完 Enum 之後,接著我們可以透過  Dictionary  定義 switch case 中所要執行的動作,在 lookup 字典中 key 為剛剛新增的 Enum 類別,value 在範例中是透過 console 因此使用 action 委派,將這些要執行的動作逐一的加到自訂的 _heroHandleLookup 中
    private Dictionary<Hero, Action> _heroHandleLookup;
    private void InitialHandler()
    {
        _heroHandleLookup = new Dictionary<Hero, Action>()
        {
            {Hero.Thanos, ()=> Console.WriteLine("I am villainous") },
            {Hero.CaptainAmerica, ()=> Console.Write("Captain America")},
            {Hero.IronMan, ()=> Console.Write($"I am IronMan") },
            {Hero.Thor, ()=> Console.Write($"I am Thor") },
            {Hero.DoctorStrange, () => Console.Write("I am DoctorStrange")},
            {Hero.Hulk, () => Console.Write("我綠綠的") },
            {Hero.SpiderMan, () => Console.Write("我會不自覺劇透")}
        };
    }

    取代 switch case
    在回到原本的代碼,可以透過 dictionary.containsKey 先判斷要執行的 Enum 是否存在 lookup 中,如果有的話就使用定義好的 action 委派來執行,代碼調整如下
    static void Main(string[] args)
    {
        InitialHandler();
    
        var hero = Hero.IronMan;
    
        if (_heroHandleLookup.ContainsKey(hero))
        {
            _heroHandleLookup[hero].Invoke();
        };
    }
    輸出結果,宣告重構成功 ! 
    I am IronMan

    重構 - 使用 Func
    透過以上描述,相信對於透過 dictionary 來重構複雜的 switch case 有一定的了解,但現實生活中往往是殘酷的,Production 的代碼中不會只有 console.write 那麼的簡單,很常會遇到 switch case 有回傳值的狀況發生,如果熟悉 LINQ 的朋友可以了解在 LINQ 很多 API 設計上都有用到 Func 的特性來讓開發者在使用時更為彈性,Action 與 Func 的差異差在 Func 有回傳值 Action 沒有,因此如果當有回傳值的時候就可以從原本的 Action 改用 Func 委派來解決回傳值的問題,代碼調整如下
    private static Dictionary<Hero, Func<string,string>> _heroHandleLookup;
    private static void InitialHandler()
    {
        _heroHandleLookup = new Dictionary<Hero, Func<string,string>>()
        {
            {Hero.Thanos, s => $"I am {s}" },
            {Hero.CaptainAmerica, s=> "Captain America"},
            {Hero.IronMan, s => "I am IronMan" },
            {Hero.Thor, s => "I am Thor" },
            {Hero.DoctorStrange, s => "I am DoctorStrange"},
            {Hero.Hulk, s => $"{s} 綠綠的rrrrrrrrr" },
            {Hero.SpiderMan, s => "我會不自覺劇透"}
        };
    }
    
    static void Main(string[] args)
    {
        InitialHandler();
    
        var hero = Hero.Hulk;
        var result = string.Empty;
    
        if (_heroHandleLookup.ContainsKey(hero))
        {
            result = _heroHandleLookup[hero].Invoke(hero.ToString());
        };
    
        Console.WriteLine(result);
        Console.ReadKey();
    }
    代碼說明如下
  • lookup 的 Dictionary 的 value 由 action 改為 Func<string,string>
  • 以此情境來說 Func 第一個參數為要帶入的參數,第二個參數為回傳值
  • 透過 lambda 指定各自的回傳值,舉例來說 Iron 就回傳 I am IronMan,Hulk 回傳 {參數} 綠綠的rrrrrrrrr
  • 在使用端使用 result 來接回傳值,並統一透過 console 顯示回傳結果
  • 在重新執行一次結果並改用 hunk,輸出結果為 
    Hulk 綠綠的rrrrrrrrr

    感想
    會寫這一篇是因為最近大部分時間是在公司既有專案的維護,看到專案中有很多 code smell 程式碼的壞味道,像是 switch case 就是其中一大項目之一,雖然之前上課就學過使用此重構技巧,但對此特別有感而發希望將此過程記錄下來,希望這篇文章可以幫助到有興趣的朋友,如果有不清楚的地方歡迎一起討論,hope it helps !


    參考
    Saving a few lines of code. Part IV - Refactoring switch statements
    How to work with Action, Func, and Predicate delegates in C#

    3 則留言:

    1. 整理的不錯,有幫助到我,謝謝

      回覆刪除
    2. 如果可以使用C# 8.0語法的話 (https://docs.microsoft.com/zh-tw/dotnet/csharp/whats-new/csharp-8#switch-expressions) switch 可以簡化許多:

      public static class HeroExtensions
      {
      public static string ToCustomString(this Hero hero)
      => hero switch
      {
      Hero.Thanos => $"I am {hero}",
      Hero.CaptainAmerica => "Captain America",
      Hero.IronMan => "I am IronMan",
      Hero.Thor => "I am Thor",
      Hero.DoctorStrange => "I am DoctorStrange",
      Hero.Hulk => $"{hero} 綠綠的rrrrrrrrr",
      Hero.SpiderMan => "我會不自覺劇透",
      _ => throw new NotImplementedException(),
      };
      }

      static void Main(string[] args)
      {
      var hero = Hero.Hulk;
      var result = hero.ToCustomString();

      Console.WriteLine(result);
      Console.ReadKey();
      }

      回覆刪除
    3. 題外話,不建議使用enum作為dictionary key,會有boxing/unboxing的效能問題

      回覆刪除

    Copyright © m@rcus 學習筆記 | Powered by Blogger

    Design by Anders Noren | Blogger Theme by NewBloggerThemes.com