在開發程式時使用判斷式是很常見的事情,簡單的判斷式可以使用 if 達到目的,但遇到多個條件或是三個以上時就可以使用 switch case 來解決此問題,但隨著需求的新增團隊可能在 switch case 的每個判斷句又加了很多新代碼,(最近在維護公司的專案上很常見到感觸很深),舉例來說可能代碼會長得像下面這樣
這篇就針對此問題重構方式做簡單介紹,若有更好的方式歡迎隨時提出來一起討論。
從上述代碼可以感受到會有程式碼的壞味道,舉例來說可能存在以下問題
定義 Enum
在 switch case 看到在進行判斷時是使用 string 作為依據,但在可能會存在像是判斷大小寫、打錯字、去空白等一些小問題,因此自己在遇到此問題的第一步會先將 switch 需要判斷的項目抽成 Enum,這樣在後續會更方便使用,也不會有打錯字的問題發生
public enum Hero
{
Thanos,
CaptainAmerica,
IronMan,
Thor,
DoctorStrange,
Hulk,
SpiderMan
}
定義 Lookup
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("我會不自覺劇透")}
};
}
在回到原本的代碼,可以透過 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,輸出結果為
感想
會寫這一篇是因為最近大部分時間是在公司既有專案的維護,看到專案中有很多 code smell 程式碼的壞味道,像是 switch case 就是其中一大項目之一,雖然之前上課就學過使用此重構技巧,但對此特別有感而發希望將此過程記錄下來,希望這篇文章可以幫助到有興趣的朋友,如果有不清楚的地方歡迎一起討論,hope it helps !
Hulk 綠綠的rrrrrrrrr
感想
會寫這一篇是因為最近大部分時間是在公司既有專案的維護,看到專案中有很多 code smell 程式碼的壞味道,像是 switch case 就是其中一大項目之一,雖然之前上課就學過使用此重構技巧,但對此特別有感而發希望將此過程記錄下來,希望這篇文章可以幫助到有興趣的朋友,如果有不清楚的地方歡迎一起討論,hope it helps !
參考
Saving a few lines of code. Part IV - Refactoring switch statements

整理的不錯,有幫助到我,謝謝
回覆刪除如果可以使用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();
}
題外話,不建議使用enum作為dictionary key,會有boxing/unboxing的效能問題
回覆刪除