2012年4月26日

白話 Design Pattern (三) Abstract Factory, Reflection Factory

繼上篇的工廠方法模式,如果你所面對的世界變得更複雜了,那該怎麼辦?

以學生餐廳的例子,原本的學生的需求很簡單,對餐點只有一個『吃』,因此只有點炒飯或魯麵的問題。

但是今天有家長來反應:『吃麵不是有湯喝嗎?那炒飯呢?我的寶貝兒子吃了你們的炒飯,結果差點渴死,你們怎麼負責?想上電視嗎?』於是校長立刻指示,今後餐點除了主食,也要有湯品。(本例純屬虛構)

重點是本來學生只要『吃』,現在增加『喝』的行為。先看下圖為原先工廠方法。

現在會進化成下圖。

意即每個工廠(點餐台)將產生一系列的物件(炒飯與附湯、湯麵與湯麵的湯﹚,以滿足Client的多個需求。日後如果食物增加了鍋貼,自然也要有對應的湯品。

上圖即為抽象工廠模式(Abstract Factory Pattern),其重點是它可以產生一系列的物件,維護同一系列物件之間的關係,而不會讓炒飯配到湯麵的湯。當然它可以更複雜,比如還要增加飯後『甜點』,這樣每個點餐台便會有三個方法(Method),但道理都是一樣的。

而上例是因為客戶端(學生)多了一種行為,導致原先的工廠方法需要擴充。實務上常常是從另一個角度發展出來,例如你原先設計了一套系統可以處理會員、產品、交易資料,結果公司業務擴充後,多出了一個新的產品線,可是兩個產品線的會員處理規則、產品呈現方式、交易行為皆不同。這樣可能原先根本不需要工廠方法的,就要開始拉出抽象的會員介面、產品介面、交易介面,然後去作抽象化的處理。

也許這時候你會寧願把整套系統Copy一份再修改,覺得比較容易些。但是別忘了,下次可能又有新的產品線等著你。而每複製一份系統,將來的維護成本可是要倍增的(萬一發現了一個bug要更新到每一套已經客製的產品裡,除非你的工作是計件算酬勞的,要不然一定會後悔)。

以上的工廠方法、抽象工廠模式,都在實踐一個重要的觀念--把客戶端實際產生的物件(產品)隔離,便於日後的擴充不互相影響。如一開始的例子:從會員擴充出黃金會員後,將來若還會有鑽石會員,也能將現有修改降至最小。



但,它們在實際使用上還是有一個缺點:客戶端必須在設計時期(Design Time)指定自己要哪一個工廠。就像走進學生餐廳的學生,每個人都天生就要決定自己該吃麵還是飯,而且不能改。

那遇到了會員資料的例子,我們由資料庫讀出一筆會員資料才知道他是哪一種會員,要怎麼辦?只能使用簡單工廠模式?

是否生成A工廠或B工廠,可以更動態的決定?

這就要講到上篇文章提到的反射(reflection)技術,它能藉由傳入組件名稱、物件名稱在執行時期(Run Time)來產生物件,如果搭配簡單工廠,便能不需要原本的判斷式,直接把物件名稱當變數傳入。也有人稱這樣的做法為反射工廠

程式大致如下:

    public class ReflectionFactory
    {
        public Product CreateProduct(string ProductName)
        {
            return(Assembly.Load("組件名稱").CreateInstance(ProductName));
        }
    }

如此,當你遇到普通會員,就傳入"Member"字串,產生出Member物件;如果是黃金會員就傳入"GoldMember",產生 GoldMember 物件,而這個字串也可以來自資料庫的欄位,這樣一來就能靈活解決之前的問題了。當然你可以用它配合抽象工廠模式,傳入要實作的工廠名稱,來解決『一系列物件』的問題。

P.S.反射並不是每一種程式語言都支援,並且效能上需要付出相對代價,詳細使用方式請善用搜尋大法。

2012年4月19日

白話 Design Pattern (二) Simple Factory, Factory Method

在GoF的書中將23個Design Patterns分為三類,分別為建構類模式(Creational Patterns)、結構類模式(Structural Patterns)、行為類模式(Behavioral Patterns)。這樣劃分簡單且清楚,就由『 建構類模式(Creational Patterns)』開始探討吧!

建構類模式,顧名思義此類的模式目的在建立出物件實體(instantiate)。

建立物件實體?為什麼還要設計模式?不是用一個 "new" 就好了嗎?

試想以下的實例:
阿甘維護了會員一個系統,原本只有一個類別(class)--Member。今天主管來告訴你,公司現在要多出一種會員身分--黃金會員,他們可以在結帳時享受更優惠的折扣。

阿甘不疾不徐的想了一下,打算這樣做:
1.增加一個新的類別:GoldMember來繼承Member。
2.覆寫結帳時的折扣規則。
3.在建立物件時作個判斷:如果該會員是黃金會員則 new GoldMember( ),否則 new Member( )。

  public class Member
    {
        public decimal GetDiscount()
        {
            ...
        }
    }

    public class GoldMember : Member
    {
        public override decimal GetDiscount()
        {
            ...
        }
    }


    if(會員是金卡會員)
    {
        new GoldMember();
    }
    else
    {
        new Member();
    }

因為計價程式會呼叫折扣函數GetDiscount(),所以完全不用改。

太棒了!看來一切可行!

這時他也注意到了出現 new Member( ) 的程式不只一個地方(可能出現在客服介面、帳單管理介面、會員網站等地方),『那就使用拿手的Copy and Paste大法吧!』,很快的這些地方都被換成以上的程式段。

此時,他發現走到身後的主管正在看著,眼角泛著晶瑩的淚光,『一定是我的工作效率太棒了,使用了OO的優點,一下子就把問題解決了,讓主管不禁留下欣慰的眼淚吧!』阿甘不禁得意起來。

如果你正是主管,發現他的問題了嗎?

Copy不是該死,但會把維護的人搞死!

是的,把相同的程式片段分散在多數地方,絕對是開發的大忌。如果日後公司又要增加一種鑽石會員,那這些片段豈不是要一個個的修改,若有地方疏漏就會造成問題,況且需求變動就要到處修改原先的程式,更是違反了OCP(開放封閉原則)

即便上述的程式沒有重複出現,把應該產生哪種會員的規則放到某處,造成該處多了增加新的會員身分就需變動的理由,也是違反了SRP(單一職則原則)

那就把該建立哪一個會員的程式片段抽出來吧!

此外,如果你還想到DIP(依賴倒轉)原則,而能將GoldMember與Member的繼承關係,改為一起繼承一個抽象型別(abstract class)或介面(interface)...(其實少用繼承也是SOLID外的開發原則,以後有機會再提吧!)

賓果!你已經進入Design Patterns的思考領域了!!


以上就是簡單工廠模式(Simple Factory Pattern)的圖示。

簡單工廠模式又稱靜態工廠模式,簡單的說就是把建立哪一個Product的判斷,放在一個專門的類別裡,而它傳回的型態即是將產品抽象後的abstract class或interface,可以說是一個簡單又好用的模式(雖然它並非GoF提出的23個之中)。

這裡使用了『工廠(Factory)』這個名詞,是把其建構出來的物件當成產品,因而自稱建構者為工廠。

大致程式如下:

    public class SimpleFactory
    {
        public Product CreateProduct(string ProductName)
        {
            if(ProductName == "A")
            {
                return(new ProductA());
            }
            else if (ProductName == "B")
            {
                return (new ProductB());
            }
            else
            {
                return(null);
            }
        }
    }

    public interface Product
    {
    }

    public class ProductA : Product
    {
    }

    public class ProductB : Product
    {
    }

但是,它並沒有百分之百滿足OCP,因為每次多了一種新的產品,就必須修改這個判斷式的部分。這時有一個改良作法是使用反射(reflection),但這個先不在此探討。

另一個改良法則是工廠方法模式(Factory Method Pattern),將工廠的部分也抽象化。這樣一來會產生多個對應產品的工廠,此後擴充產品時亦要擴充相對的工廠,看起來複雜的多。


要徹底了解差異,可以舉一個生活實例:

某學校的學生餐廳有兩個廚師-王師傅與周師傅,王師傅負責炒飯,周師傅負責煮麵。餐廳點餐的模式有以下三種:

(1)沒有Patterns:學生要自己知道自己想吃的東西是哪個師傅作的,然後自己去點。
萬一哪天其中一個師傅換人了,則所有的學生都要知道自己是不是該找新的廚師。

(2)簡單工廠模式:餐廳設置了一個點餐台,後方站一位正妹服務員,櫃台前印著精美的Menu。學生向服務員點餐後,該員再向後台的廚師請求。無論廚師怎麼換,學生找的都是這個點餐台。

(3)工廠方法模式:餐廳設計成料理東西軍模式。左邊是點飯的服務台,上面大大的招牌寫『天下無敵蛋炒飯 』;右邊是點麵的服務台,掛著『世界第一大魯麵』,學生想吃飯就向左走,吃麵的向右走。中間還可以放個人型立牌,寫上『今天你想吃哪一道,Dochi?』



想想,如果今天餐廳要加賣『鍋貼』,則以上三種情況差別在哪?

(1)沒有Patterns:需要點鍋貼的學生都要更新一下資訊,認識新的鍋貼師傅。

(2)簡單工廠模式:點餐台的服務員需要重新訓練,櫃台前的Menu要重新製作,將鍋貼加上去。學生則維持走向點餐台即可。

(3)工廠方法模式:在餐廳中間加設一個鍋貼點餐台,原先兩邊的點餐台完全不用動(符合開放封閉原則)。學生則各自走向正確的點餐台。


又, 『鍋貼』 改由周師傅負責製作,煮麵的則另聘一位林師傅,會有甚麼改變?

(1)沒有Patterns:學生又要更新一次資訊。部分改不過來的學生也許因此崩潰...

(2)簡單工廠模式:點餐台的服務員需要重新訓練,學生無影響。

(3)工廠方法模式:點麵、鍋貼的服務員重新訓練就好(點飯的服務員完全不影響),學生無影響。

由此可知,開發時期若使用了較有彈性的結構,可能讓開發成本提高,但對於之後的設計變更,便能以較小的代價因應。

那麼結構越複雜的模式一定越好?當然不是,如果你知道這個部分將來變動的機會很低,其實也沒必要殺雞卻急著打造一把牛刀。也許等將來遇到需要的時候,再來重構就好。

重點是,如果你不懂這些設計模式,你的評估一定不夠周詳。


白話 Design Pattern (一) 一些觀念

網路上關於 Design Pattern 的文章不少,當然亦有不少精闢的見解。

不過,由於每個人的領悟方式不盡相同,此系列嘗試用自己認為比較『白話』的方式,來解釋Design Pattern,並且會著重在Pattern的用途與差異(因有感這部分在網路上較缺乏),詳細作法則可以查詢其他網路文章。

以下是寫在前面的一些觀念:

1.Design Pattern可說是一些有經驗的設計者,針對某些的情況所提出的解決方式。使用上未必要照本宣科的如法炮製,只要了解其觀念、思考方向,也可以有許多變形的作法。
2.實務上,解決一個問題常不會只套用一個Pattern,所以Pattern 間不是互相排斥的,可能需要互相合作
3.單看圖示結構,有許多Pattern會非常相似,想要徹底應用,最好先了解其目的(要解決的問題)。
4.很多情況,要使用哪個Pattern 不會有唯一解,所以還是一句『看需求、看情況決定』。
5.使用Pattern是需要付出代價的(如:複雜度、效率、開發時程),也許對開始的開發時期沒有幫助,但換來的通常是變更、維護時期的好處(如:易修改、較好的擴充性、可讀性)。
6.不使用Pattern 也未必不好,需不需要使用可以用SOLID原則檢視與評估。
7.承上,不用一開始就過度設計,但記得最重要的心訣--重構(Refactoring),隨時保持要接受重構的心。

『X!原來D賽配湯(Design Pattern)架簡單!』是本系列文章的目標。(X = 讚、耶、哦...  別想歪)

本系列文章,僅建構在個人的想法,不保證有充分的學理依據。如有謬誤,歡迎指正與討論。