2012年10月19日

白話 Design Pattern (十二) Template Method Pattern

Template Method Pattern應用了OO最重要的特性之一『繼承』,來達到封裝演算法、程式reuse的目的.對我而言,它算是最容易理解的模式,甚至在沒聽過什麼是Design Pattern前,就已經自然而然的開始使用了.

假設在一個學校單位的資訊系統,其中有幾個程式片段:

成績判斷
if 身分=大學生
  if 成績 >= 60 
    IsPass = true
  else
     IsPass = false
else身分= 研究生
  if 成績 >= 70 
     IsPass = true
 else
     IsPass = true

選課限制
if 身分=大學生
  max學分 = 22
else身分= 研究生
 max學分 = 12

繳費
if 身分=大學生
  學費 = 20000
else身分= 研究生
  學費 = 1000*學分

這種情形應該很常見,看起來也沒什麼不合理.那會有甚麼問題?

一、擴充不易
如果今天學校要招收博士班學生了,那麼相關的片段可能又都要加上身分=博士班學生的判斷,往後招收夜間部、旁聽生...每次都要增加新的判斷式.

二、邏輯分散
同上,也因為程式片段分散多處,增加或修改的時候一定不能有漏,否則可能出現無法預期的狀況...

三、維護不易
假設今天大學生的選課限制要修改,改成大一到大三是22學分,大四則可以25學分,或更複雜的規則.因為各種身分的選課規則都放在一起,一不小心說不定大學生部分改好了,卻動到研究生的規則,而造成其他錯誤.

說到底還是因為以上方法違反了OCP(Open Close Principle),這時候就應該考慮使用適當的Design Pattern來改善.甚至有一種說法,如果系統中同一種判斷(如例子中的『身分』)出現兩次以上,就應該考慮將其重構.(好吧!這是我自己這麼想的)

解決以上的問題,最簡單的做法就是將建立一個學生抽象類別(Abstract Class),然後讓大學生、研究生繼承之,並將以上程式片段改成呼叫學生子物件.


這麼一來,將來要繼續擴充更多種學生,既不需要更動業務規則,也不怕影響原有的其他學生子類別,簡簡單單就符合OCP.

而實際使用中,在這些程式片段之前,會有一個問題-『學生物件如何生成?』這就可以參考前面的簡單工廠或工廠模式.特別提一下,工廠模式本身也是Template Method Pattern的特例.

當然,這個模式享受了使用繼承的好處,但也可能因此帶來其他問題,否則也不會有其他模式的存在了,更何況還有『少用繼承,多用合成』這樣的設計原則,這就在下一章介紹了.


2012年8月21日

求才若渴


如果您對系統開發有熱情,卻覺得價值在工作中無法展現。
如果您崇尚發揮創意、減少加班的敏捷式開發文化。
如果您了解或對Design Pattern有興趣,卻無實戰機會。

歡迎留下連絡方式或mail到jhchen@sce.pccu.edu.tw
(開發工具以ASP .Net、C#為主)

========================================

從事系統開發已超過10年的我
希望打造一個軟體開發人員都能發揮自身價值
也能讓價值被看見的團隊
(目前已稍有點樣子)

也因此,最近開始引進敏捷式開發
並研究一些軟體工程技術(如:Design Pattern、重構...)

希望同樣有熱情的人不要在台灣不受重視的環境中
被消磨殆盡

2012年7月12日

Page與 User Control的事件發生順序(.Net Framework 4.0)

這個對於寫控制項應該很有幫助
需要時候找不到還滿麻煩的
所以就先抓下來

如果你在user control中有個DropDownList(或GridView等其他控制項)
也設了DataSource
但是在Page中的Page_Load抓不到值
看了下圖就應該知道這是正常的
因為當時Control還未data binding
改為在PreRenderComplete再抓資料即可

其他常用的如:PreInit, Init, PreLoad, Load, PreRender等順序如下圖

原網址:ASP.NET Page Life Cycle Overview (MSDN)
有很詳細介紹




2012年5月11日

白話 Design Pattern (七) Adapter Pattern, Abstract Server Pattern

想像一下,如果今天你的主管交代了一個任務,要你從一台老舊的電腦找一份遠古時期的資料。你試了一下,很幸運的它還能開機,但是,它竟然沒有USB介面來連接你的滑鼠,而這個年代,已經沒有人生產PS/2滑鼠了,要怎麼辦?難道要到拍賣場找一隻堪用的嗎?

如果你知道有『USB to PS/2 轉接頭』這樣的東西,相信這個問題根本不是問題。

事實上,生活中我們的確使用了許許多多的轉接頭,如:電源線三孔轉兩孔、手機用micro usb 轉 usb線、出國用萬用插座...,因為有了轉接器,讓我們可以在不同規格間使用某些設備。

Adapter Pattern正是轉接器。主要目的便是藉由轉接方式,解決介面不同的問題。它的結構也很簡單,就是一頭接我們要的介面,另一頭接提供功能的元件,然後作對應。




使用時機:通常都不是自己新開發的系統,而是為了讓舊有的元件得以繼續運作,或是為了某些外購的元件。


==================================

在Agile Software Development, Principles, Patterns, and Practices一書中,另外提到一個可搭配Adapter的Abstract Server Pattern,它一樣很簡單,卻也是常出現的基本概念。

Abstract Server Pattern就是在原本直接的關聯中加入一個抽象,好讓物件間的藕合度降低。簡單的說,就是DIP(依賴倒轉)原則。

書中的例子(圖上部),原本的設計將『燈』與『開關』直接關聯,這樣一來有開關就必須有燈不可。加入一個interface後,就能將其間的關聯性減輕,日後如果電扇要用這個開關,也只要繼承抽象的interface即可。



如果考慮到使用開關的物件可能來自外部元件或舊有的元件,而無法遵循現在開發的interface,那搭配了Adapter就能進一步達成(如下圖)。



本文將這些模式說得很簡單,其實也是想說其實設計模式未必是艱澀難懂,或需要強記的,事實上,即使是沒聽過設計模式的人,如果在撰寫程式時,考慮了減輕耦合、增加彈性之類的概念,也有可能設計出接近這樣的結構。

學習設計模式不是為了找地方套用這些架構,而是你遇到難以取捨之處,可以參考看看你的設計是否考慮周詳,故了解每個模式的精神、形成原因才是最重要的。



2012年5月9日

白話 Design Pattern (四) Builder pattern


相較於工廠系列的模式,著重在該產生哪一個物件;我們也可能面臨到建構過程本身就複雜的狀況,這時如何組裝出一個物件就可能是要解決的問題了,這裡要提到的是建構者模式(Builder pattern)

以學生餐廳的例子,如果將聚焦在點餐櫃台的動作,我們發現服務人員在接獲點餐後,分別有以下幾個動作:

炒飯櫃台:1.放上餐盤 、2. 放上免洗筷與鐵湯匙、3.放上炒飯、4.放上附湯。

麵點櫃台:1.放上餐盤 、2. 放上筷子與長湯匙、3.放上麵品、4.放上飲料 。


想想看,怎麼作比較好?

1.這次的重點在餐點的產生過程比較複雜,最好能夠將過程侷限在服務人員身上,點餐的學生只要享用該完成品,不需知道,也不需參予整個過程。

2.仔細分析,櫃台人員都有相同的四個動作,抽象的描述為:
1) 放上餐盤、2) 放上餐具、3) 放上餐點、4) 放上附湯飲料。

3.所以我們可以想像,整個模型有兩個部分:

1)執行四個動作:這部分兩個櫃台都一樣,見下圖的點餐服務

2) 對四個動作的各自表述:有一個抽象的餐點Builder與繼承它的炒飯Builder、麵食Builder


故,Builder Pattern的類別圖如下:

其中的Director就是負責執行各步驟並產出產品,Builder則確保了其繼承者都會有N個步驟。也有人將Director合併到Builder內,成為一種較簡化的變形。而這個模式中的幾個建構步驟,也可能與工廠系列的模式合作。

總之,Builder模式的重點有:

1.生產的流程複雜,需要被封裝。

2.每個builder都有相同的N個步驟或零件(步驟有先後順序,零件則無)。


前述的用法,產品較類似套餐,即各步驟或零件是固定的,選了A套餐,便能決定了每個部分是什麼。但實務上,如果到速食店點餐,雖然有分:主餐、副食、飲料三個部分,但可能個部分都是消費者當下選擇的(主餐可選雞塊/漢堡/雞堡、副食可選薯條/薯餅...),那就不可能在設計時期(Design Time)把所有的組合Builder都寫好,而需要代入多個參數,像下列:

餐點 A = new 餐點Builder(主食.雞塊, 副食.薯條, 飲料.coca).build();

那麼,如果建構時所需的參數非常多,要怎麼辦?下例是使用了Fluent Interface的技巧,讓新增客戶資料時,可以像以下這樣,簡潔易讀。


客戶 A = new 客戶Builder()
            .Name("Rock")
            .email("xxx@ooo.com")
            .cellphone("09xx123456")
            .build();

大致的寫法是:


public class 客戶
{
   public 客戶(string ID, string Name, string email, string cellphone)
   {
    ...
   }
}

public class 客戶Builder
{
   private 客戶 _Customer;

   string _Name;
   string _email;
   string _cellphone;

   public string Name(string Name)
   {
      _Name = Name;
      return(this);
   }

   public string email(string email)
   {
      _email = email;
      return(this);
   }

   public string cellphone (string cellphone)
   {
      _cellphone = cellphone;
      return(this);
   }

   public 客戶 build()
   {
      _Customer = new 客戶(_Name, _email, _cellphone);
      return(_Customer);
   }

}


最後,如果有人因為看到Builder中包含了多個建置方法,而覺得會跟Abstract Factory搞混,這裡整理了Builder Pattern與Abstract Factory Pattern的不同:

1) Builder著重在隱藏複雜的建置步驟,最後只傳回一個產品。

2) Abstract Factory則是為了維護一系列產品的關聯,會產出某系列的多項產品。

3) Builder模式中,Client不需要認識各個零件的型態。(只要『吃』產出的餐點)

4) Abstract Factory中,Client認識各項的抽象型別或介面,並能使用它們。



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)工廠方法模式:點麵、鍋貼的服務員重新訓練就好(點飯的服務員完全不影響),學生無影響。

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

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

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