[設計模式-1] 簡單工廠模式

話三國之~簡單工廠模式

前言

不知不覺也做程式設計師一段日子了,時間真的過很快。當自己還是新手的時候,想盡辦法達到主管的需求是我唯一追求的目標,那時候還蠻寫喜歡直覺式寫程式碼的,因為一條腸子通到底的寫法真的還蠻容易的,可能是因為比較不用動腦吧XD。但隨著經驗的增長,心裡開始有一點小小的夢想,就是希望能寫出一個讓人讚嘆不已的偉大軟體。所以,我開始思考,程式碼真的只是為了達到需求嗎?現在我的答案是,一半對一半錯,程式碼要達到需求?當然要!這是最基本的,否則你有再偉大的架構,但功能錯誤,那還不如很亂、很醜、很難維護卻達到系統需求功能的程式碼來得有價值。

但程式碼沒有錯誤,就是最高境界了嗎?我想那也是不對的,其實,自己工作那麼多年,常聽到大家抱怨,當然我自己也常抱怨:天阿!這又是哪個神人寫的神邏輯!簡直不堪入目。但這些程式的確是很正常的在運作唷,可是當需求變更時,想要修改,我只能說,那是個噩夢!但我雖然很想推廣物件導向的寫法,可也發現這是有障礙的,因為不是每個人都懂,甚至上層也來關切,說如果這個東西讓人很難理解,還不如就直接全部寫成一大包,一條腸子通到底就好,而且在時程壓力下,大家都認為用直覺式的寫法是最快速的,交接的人只要一行一行偵錯也還是可以看懂整個邏輯。其實,我很難解釋這個東西到底好在哪,在以往,這些觀念是資深程式設計師的壓箱寶,不是他們不願意分享,而是這些東西必須要透過經驗累積。高手程式設計師很難告訴你,為何在某個情境下要處理掉兩個物件過度的依賴而必須採用這種設計。因為那是經驗。就像是武當派的張三豐一直都是金庸小說裡幾乎最強的角色,但他的徒弟武當七俠卻只能算二流角色一樣,以張三豐跟徒弟們的感情,他不可能去藏私,一定會盡心盡意的想分享他的武功給弟子,但這些東西就是沒辦法不透過實戰告訴你。

但現在學習寫程式的人真的很幸福,高手已經把那些壓箱寶的經驗化做一條一條的理論,講理論總能記了吧?我相信是能的。但這樣又會變成另外一個金庸筆下的角色王語嫣,雖然精通天下武學,本身卻無法與敵人對陣。相信這也不是大家所希望的。因此,結合實戰似乎是一條必經之路。但要如何結合實戰呢?其實在生活周遭,處處是設計模式。所以,設計模式我認為它不是發明而是發現,這樣講也許很攏統,所以我便興起了以歷史故事的方式來分享設計模式,我想大家看那種整篇都在講理論的設計模式書籍,多半會看到想睡著。但歷史故事是大家都喜歡的,也是大家耳熟能詳的,我覺得藉由故事的方式應該可以引出對設計模式的興趣,並且結合了實際的歷史場景和劇情,真的把它想通透過一遍之後,遇到一樣的情境,你便能很直覺的知道該如何做設計。就像是武林高手一樣,在與敵人對戰的過程,他們其實都是靠著身體反射動作在戰鬥,如果你要等到判斷出敵人的招式,然後思考該如何對抗,對方已經一劍把你刺死了。

講了那麼多,大家準備好來一趟設計模式的歷史故事之旅了嗎?我本身是個三國迷,因此,我應該都會採用三國體材,三國故事應該是無人不知無人不曉吧?好,廢話不多說,那我們就開始吧。

諸葛亮痛哭龐統 

這個時間點的背景,大概是在赤壁之戰後,劉備取得荊州聲勢日漸浩大,東吳於赤壁之戰後寸土未得,不禁眼紅萬分而向劉備不斷索要荊州,劉備以若歸還荊州 就沒立足之地為由拒絕,但為了安撫孫權則立下一個承諾,若取得西川 即歸還荊州。劉皇叔言出必行,不久便出兵了西川,一路勢如破竹,豈知 驕兵者敗、欲速則不達,竟在一路順風攻勢的狀況下,中了埋伏吃了大敗戰。連軍師龐統也陣亡了,不得已只好遠召諸葛亮來援。

諸葛亮原擔任荊州防務,正愁不知派何人接任,關羽向前答道:願擔此重任。諸葛亮其實深知關羽並不適合,因關羽太過自大輕視天下英雄,一整個老子天下第一的想法,遲早要吃足苦頭。但受制於桃園三結義的小圈圈,諸葛亮其實對關羽也是得尊重三分,關羽的能力,諸葛亮也是知道的,但就個性來說,關羽絕非最佳人選,不得已,只好來面試一下。於是便向關羽說,荊州乃我立足之本不可有失,將軍可知道?關羽答:關某將誓死守護!諸葛亮聽到死字,不是很滿意又再追問:倘曹軍引兵來犯 何解?羽答道:引兵拒之!諸葛亮面露難色又問:倘吳軍引兵來犯 何解?羽答道:引兵拒之!心裡暗想,這三小問題,當我三歲小孩阿,廢話,要不然勒? 不過也不便發作

如果以最簡單的程式碼來表示當時諸葛亮內心的想法,和關羽的答案,程式碼大概如下

 class Program
    {

        static void Main(string[] args)
        {
            //關羽的防守策略將依照曹操和孫權的動態而決定
            Console.Write("請輸入曹操和孫權的動態");
            string State = Console.ReadLine();

            string strategy= "";
            string Result = "";
            switch (State)
            {
                case "曹操來犯":
                    strategy= "北抗曹操";
                    Result = "遭孫權夾攻,荊州必失";
                    break;

                case "孫權來犯":
                    strategy= "東抗孫權";
                    Result = "遭曹操夾攻,荊州必失";
                    break;
            }
            Console.WriteLine("關羽採用:" + strategy+ "結果:" + Result);
        }
    }

程式碼很輕易的就達到目的,這也是程式設計師最會寫的程式類型,但這個程式碼卻有很嚴重的壞味道。首先,它並沒有面向介面來寫程式碼,再來在程式碼內看到判斷條件是判斷寫死的字串值,本身就似乎看起來很不舒服。但不能單純就以這樣的理由說這段程式碼是不好的,必須要還說出為什麼,才代表是真的懂。

真正好的程式碼是在達成需求的前提下,同時又滿足好維護、好擴展、可共用且具備彈性。那到底要如何做呢?一言以蔽之,繼承,封裝,多型,這就像是劍法的刺、挑、砍,再怎厲害的劍法,都是由這三種基本動作演化而來。以上程式碼,我先問一個最簡單的問題,假如今天,諸葛亮問完關羽之後,又去問關羽的義子關平,我們假設關平他是百分之百贊同父親的戰略構想,那上述程式碼其實只有最後一句的輸出語法需改成"關平採用",一般而言,大家會怎改?簡單嘛~複製一份關羽的程式碼,貼上然後把"關羽"改成"關平",一切完美結束。這種作法,任誰都會覺得這樣做不對,但很奇怪,大部分的程式設計師卻又都這樣做。

 class Program
    {

        static void Main(string[] args)
        {
            //關羽的防守策略將依照曹操和孫權的動態而決定
            Console.Write("請輸入曹操和孫權的動態");
            string State = Console.ReadLine();

            string strategy= "";
            string Result = "";
            switch (State)
            {
                case "曹操來犯":
                    strategy= "北抗曹操";
                    Result = "遭孫權夾攻,荊州必失";
                    break;

                case "孫權來犯":
                    strategy= "東抗孫權";
                    Result = "遭曹操夾攻,荊州必失";
                    break;
            }
            Console.WriteLine("關平採用:" + strategy+ "結果:" + Result);
        }
    }

看,多簡單阿,我只花了三秒,我的程式就寫好了。但,唉,不想再講了,這是身為一個程式設計師的原則問題。這裡就要講到"重複使用"的重要性,這其實也是達成偉大軟體設計的五大原則之一"不要重複寫一樣的程式碼",而想到不重複,第一個反射動作就要想到封裝,藉由業務邏輯的封裝,來讓程式碼的可用性增加。

其實,這邊針對關羽和關平還可以再做抽象化,舉例來說,關羽和關平你可以把他想像成都是實例,但他們的抽象也許可以是關家軍,畢竟它們都性關麻,或者是劉備軍,畢竟他們都效忠劉備,所以可以把屬於他們兩個共通的內容擺到這個抽象類別中共用,但這不是這裡要講的重點,就先稍微帶過。

    public class strategy
    {
        public static void Excute(string general,string state)
        {
            string strategy= "";
            string Result = "";
            switch (state)
            {
                case "曹操來犯":
                    strategy= "北抗曹操";
                    Result = "遭孫權夾攻,荊州必失";
                    break;

                case "孫權來犯":
                    strategy= "東抗孫權";
                    Result = "遭曹操夾攻,荊州必失";
                    break;
            }
            Console.WriteLine(general+"採用:" + strategy+ "結果:" + Result);
        }
    }

們把策略方針封裝起來。(也就是在面對曹操或孫權來襲時,所採取的一系列對抗方式)現在這個類別我們要吃兩個參數,第一個參數是執行方針的將軍名稱,第二個參數是目前的局勢(曹操來犯or孫權來犯),而類別提供一個方法可以呼叫,這樣一來,在主程式端就顯得簡潔許多。

諸葛亮問關羽方針的程式碼

 class Program
    {

        static void Main(string[] args)
        {
            //關羽的防守策略將依照曹操和孫權的動態而決定
            Console.Write("請輸入曹操和孫權的動態");
            string State = Console.ReadLine();
            stategy.Excute("關羽", State);
        }
    }

諸葛亮問關平的程式碼

 class Program
    {

        static void Main(string[] args)
        {
            //關羽的防守策略將依照曹操和孫權的動態而決定
            Console.Write("請輸入曹操和孫權的動態");
            string State = Console.ReadLine();
            stategy.Excute("關平", State);
        }
    }

好了,解決了重複程式碼的問題,我們故事可以繼續下去了,諸葛亮益發擔憂了:倘曹孫兩家一起來犯 何解?羽答道:分兵拒之!諸葛亮搖搖頭道:如此荊州危矣!現在跑出第三個可能性,如果兩家發兵來犯,關羽要採取分兵拒之的策略,那這樣程式碼要如何改,也許大家心中都有答案了,其實要改是也不難就是在strategy類別中加一個swich case不就解決了?那我們就直接來看吧。

public class strategy
    {
        public static void Excute(string general,string state)
        {
            string strategy= "";
            string Result = "";
            switch (state)
            {
                case "曹操來犯":
                    strategy= "北抗曹操";
                    Result = "遭孫權夾攻,荊州必失";
                    break;
                case "孫權來犯":
                    strategy= "東抗孫權";
                    Result = "遭曹操夾攻,荊州必失";
                    break;
                case "孫權曹操一起來":
                    strategy= "北抗曹操東抗孫權";
                    Result = "你以為你是誰啊,荊州必失";
                    break;
            }
            Console.WriteLine(general+"採用:" + strategy+ "結果:" + Result);
        }
    }

挖,改好了,而且只需要改一次程式碼,兩邊的程式就都改完了,這樣子寫程式真是過癮阿!是否這樣就算練成了絕世武功呢?我只能說,還差遠的呢!這邊你試想一下,我只是擴充一個新的功能,卻要動到原本"已經很正常在運作"的類別,這是很危險的一件事,這又扯到另外一個建立偉大程式碼的設計原則"對修改關閉,對擴充開放",寫程式大家應該都有過經驗,就是我只是改一個小地方,結果,以前運作很好的程式,就莫名其妙的爆炸了。所以我現在要講的就是那三大基本招式(繼承、封裝、多型)的另外兩招,繼承和多型

    /// <summary>
    /// 軍事策略方針
    /// </summary>
    public interface IDefenceStrategy
    {
        void Excute(string general);

    }

首先我們先建立一個策略的介面,只定義一個方法,接受的參數是將軍的名稱

    /// <summary>
    /// 北抗曹操 繼承IDefenceStrategy
    /// </summary>
    class DefenceFightToNorth : IDefenceStrategy
    {
        public void Excute(string general)
        {
            string Strategy= "北抗曹操";
            string Result = "遭孫權夾攻,荊州必失";
            Console.WriteLine(general + "採用:" + Strategy + "結果:" + Result);
        }
    }

    /// <summary>
    /// 東抗孫權 繼承IDefenceStrategy
    /// </summary>
    class DefenceFightToEast : IDefenceStrategy
    {
        public void Excute(string general)
        {
            string Strategy= "東抗孫權";
            string Result = "遭曹操夾攻,荊州必失";
            Console.WriteLine(general + "採用:" + Strategy+ "結果:" + Result);
        }
    }

接著我們要定義"北抗曹操"和"東抗孫權"的實作都實做了剛剛定義的IDefenceStrategy介面,所以回到剛剛的需求如果我們現在要多一個新的策略,只需要在擴充一個實作即可,原本的類別都不會被動到。

 /// <summary>
    /// 北抗曹操東抗孫權
    /// </summary>
    class DefenceFightToBoth : IDefenceStragegy
    {
        public void Excute(string general)
        {
            string stategy = "北抗曹操東抗孫權";
            string Result = "你以為你是誰啊,荊州必失";
            Console.WriteLine(general + "採用:" + stategy + "結果:" + Result);
        }
    }

講了那麼多,那到底要怎麼用呢,我只看到現在多了一堆類別,好啦,還有聽到繼承,但多型呢?別著急,首先,要講到的第一個模式便是簡單工廠模式,顧名思義,我們已經萬事具備了,就只差一個工廠。

 class StrategyFactory
    {
        public static IDefenceStragegy getStrategy(string strategyname)
        {
            //多型強大的地方,因為在判斷參數之前
            //並不知道是哪個型別,可是因為他們都實做了IDefenceStragegy
            //所以我可以宣告型別是IDefenceStragegy
            IDefenceStragegy strategy = null;
            switch (strategyname)
            {
                case "北抗曹操":
                    strategy = new DefenceFightToNorth();
                    break;
                case "東抗孫權":
                    strategy = new DefenceFightToEast();
                    break;
                case "北抗曹操東抗孫權":
                    strategy = new DefenceFightToBoth();
                    break;
            }
            return strategy;
        }
    }

多型還有一個比較通俗的說法,就是父參考操控子類別。好了,大功告成,我們以後不管有再多種策略,都只需要新增IDefenceStrategy的子類,接著在工廠類別新增一個switch case即可,而完全不會動到原本的類別。所以當功能在做擴充的時候,之前的程式邏輯被有效的保護起來。有沒有覺得很神奇呢? 剩下最後一小段,客戶端的程式碼,基本上這裡的程式碼用簡單工廠開發,就已經完全解除相依性,即使新增策略,客戶端程式碼也不會被動到

static void Main(string[] args)
        {
            //關羽的防守策略將依照曹操和孫權的動態而決定
            Console.Write("請輸入曹操和孫權的動態");
            string State = Console.ReadLine();
            IDefenceStragegy strategy= StrategyFactory.getStrategy(State);
            strategy.Excute("關羽");
        }

故事還沒有結束唷,這一節只講到簡單工廠模式,欲知後續如何發展,且看下回分解。