[.NET]重構之路系列v11 –用責任鏈模式打破討厭的switch case

[.NET]重構之路系列v11 –用責任鏈模式打破討厭的switch case
使用場景,只知道叫這條責任鏈處理,就可以得到想要的結果。

前言
有吃過虧的讀者都知道,巢狀的if, loop或是switch case,通常就是代表著複雜度的增加,健壯性的減少,彈性的下降。也通常都會違背開放/封閉原則。

今天要介紹的,就是用一個最簡單的例子,來說明怎麼使用Chain of Responsibility pattern來取代switch case的作法。

原本用switch case的程式碼


        static void Main(string[] args)
        {
            var today = DateTime.Now.DayOfWeek;

            switch (today)
            {
                case DayOfWeek.Friday:
                    //星期五,猴子去跳舞
                    Console.WriteLine("星期五,猴子去跳舞");
                    break;
                case DayOfWeek.Monday:
                    //星期一,猴子穿新衣
                    Console.WriteLine("星期一,猴子穿新衣");
                    break;
                case DayOfWeek.Saturday:
                    //星期六,猴子去斗六
                    Console.WriteLine("星期六,猴子去斗六");
                    break;
                case DayOfWeek.Sunday:
                    //星期日,猴子過生日
                    Console.WriteLine("星期日,猴子過生日");
                    break;
                case DayOfWeek.Thursday:
                    //星期四,猴子去考試
                    Console.WriteLine("星期四,猴子去考試");
                    break;
                case DayOfWeek.Tuesday:
                    //星期二,猴子肚子餓
                    Console.WriteLine("星期二,猴子肚子餓");
                    break;
                case DayOfWeek.Wednesday:
                    //星期三,猴子去爬山
                    Console.WriteLine("星期三,猴子去爬山");
                    break;
                default:
                    break;
            }
        }


責任鏈的作法
先簡單看一下責任鏈的Class Diagram:
chain of responsability implementation - uml class diagram
圖片來源:http://www.oodesign.com/chain-of-responsibility-pattern.html

簡單的說明一下,switch case的特性就是,某一個條件有很多情況,針對某一種情況時,要執行該情況的處理。以這篇的例子來說,條件就是『星期幾』,要執行的動作就是看星期幾,猴子就要做對應的動作。

而責任鏈就是把這種攤開的條件判斷,用相同的抽象Class串起來,接著把條件丟進去。每一個衍生的Class會去判斷這個條件,來決定是不是自己該作用。以這例子來說,就是會有『星期一~星期日』七隻衍生猴子的Class。星期一的猴子,會判斷丟進來的日期,是不是星期一,來決定自己要不要做事。如果,不是屬於自己要做事,那就交給下一隻猴子處理。

了解了上面的流程,請你跟我這樣做:
1. 建立一個Abstract的猴子:


    public abstract class AbstractMonkey
    {
        private AbstractMonkey _nextMonkey;
        public AbstractMonkey(AbstractMonkey monkey)
        {
            this._nextMonkey = monkey;
        }

        public void DoSomething(DayOfWeek today)
        {
            if (isMyResponsibility(today))
            {
                this.MyAction(today);
            }
            else
            {
                if (this._nextMonkey != null)
                {
                    this._nextMonkey.DoSomething(today);
                }
                else
                {
                    Console.WriteLine("責任鏈結束!");
                }

            }
        }

        protected abstract void MyAction(DayOfWeek today);
        protected abstract bool isMyResponsibility(DayOfWeek today);
    }

(1) 透過建構式來決定,下一隻猴子是誰,這樣就可以把整群猴子串起來。
(2) 公開的方法是DoSomething,供外界呼叫,也供上一隻猴子呼叫。
(3) 如何決定是不是屬於自己的職責,交給每一個子類別自行決定,所以這邊使用abstract的方法。
(4) 每一支猴子,要做的事,由他們自己決定。所以這邊MyAction也是宣告成abstract,給子類別自行決定對應的行為。

2.猴子群,這邊以星期一來說明:


    public class MonkeyMonday : AbstractMonkey
    {
        public MonkeyMonday(AbstractMonkey monkey)
            : base(monkey)
        {

        }
        protected override void MyAction(DayOfWeek today)
        {
            //星期一,猴子穿新衣
            Console.WriteLine("星期一,猴子穿新衣");
        }

        protected override bool isMyResponsibility(DayOfWeek today)
        {
            return today == DayOfWeek.Monday;
        }
    }


每一支猴子,都只專注在做自己的事,它也不會知道別人怎麼來使用它。自己的職責就是:
(1)判斷自己是不是該作用;
(2)作用時,該做什麼事;

3.應用場景重構完的結果


        static void Main(string[] args)
        {
            var today = DateTime.Now.DayOfWeek;
            
            //星期一到星期日的情況,不管今天是哪一天,就是負責那一天的猴子要動作
            var monkey = new MonkeyMonday(new MonkeyTuesday(new MonkeyWednesday(new MonkeyThursday(new MonkeyFriday(new MonkeySaturday(new MonkeySunday(null)))))));
            monkey.DoSomething(today);
        }


原本的swtich case消失了,透過MonkeyMonday串起來了。

image

結論
不是所有的switch case都需要透過責任鏈來解耦合,但不可否認的,如果每個case條件裡面要做的,都是一件『職責』,而且這樣的case條件,未來可能會增加,那麼責任鏈模式會挺適用的。

在這篇例子裡面,還帶有一點點Decorator的味道,以及Template Method的味道,而初始化那一串猴子,也可以再透過其他方式來封裝。

很多設計其實從不同角度來看,可能就是不同的pattern,所以硬要去釐清這個角度是什麼pattern,其實還不如把時間花在這樣的設計,用來解決什麼樣的問題,來得有意義的多。

20120415 補充:責任鏈的用意在於把每個角色的職責分清楚,每個物件只需要做好自己的事,透過責任鏈的方式來組合,並完成使用場景的需求。各角色的職責如下:

  1. 鏈上的每個責任物件,只知道『針對什麼情況,代表是自己的職責』,以及只知道『自己的職責,應該要做什麼事』。
  2. 抽象的責任物件,只知道『往下傳遞』這件事。
  3. 使用場景,只知道『呼叫這條責任鏈處理』,就可以得到想要的結果。


Sample Code: ChainOfResponsibilitySample.zip


blog 與課程更新內容,請前往新站位置:http://tdd.best/