在開發程式時使用判斷式是很常見的事情,簡單的判斷式可以使用 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的效能問題
回覆刪除