1、 傳授設計模式中存在的問題
我個人最近對設計模式中的工廠模式進行了比較深入的學習,通過學習感悟出現在很多設計模式傳道者,在講述設計模式的過程中存在一些問題,使得設計模式變得很難理解。設計模式本身很抽象,但是這些傳道者在講述的過程中,將一個抽象的東西講的更抽象,從而使我們望而卻步。有些人在講述的時候,沒有考慮讀者的注意力。比如我在看《C#設計模式》的時候,在講述抽象工廠模式的時候,直接就進入了一個示例,而且這個例子比較複雜,涉及了比較多的概念、術語,而且還有算法要處理。但是這些和要講述的核心內容無關,我想要看懂他的示例就要將這個東西都搞懂,就分散了我的注意力。我個人總結,學習設計模式的方法是,首先找到一個突破口,比如可以先學習構造型模式中簡單的模式,將它理解、熟練應用。通過對一、兩個模式有一個深入的認識之後,再學習其它比較複雜一點的模式就容易多了,這是一種迭代的思想在學習中的應用。另外學習任何一種設計模式的過程應該是具體-抽象-再具體這個的一個過程。這句話的意思是首先通過一個比較具體一點的例子來幫助理解設計模式,在理解之後將你的理解擴展到解決這一類問題,上升到一定的理論高度。然後就是再到具體,也就是應用設計模式,應用理論解決自己遇到的實際問題。
2、學習工廠模式的預備知識:
首先聲明這些預備知識並不是工廠模式僅僅需要,因爲我先講述工廠模式,所以在學習工廠模式之前將這些問題提出。
2.1 Upcasting:
Upcasting中文翻譯有好幾個,比如向上類型轉換、向上轉型、上溯造型。我個人比較喜歡向上轉型這個翻譯,即簡單又含義明確。向上轉型這個概念,我在Bruce Eckel在的Thinking in c++、Thinking in Java中都看到過,我不是很確定這個概念是否是他提出來的。向上轉型是將把一個派生類當作它的基類使用。我們將一個更特殊的類型轉換到一個更常規的類型,這當然是安全的。派生類是基類的一個超集。它可以包含比基類更多的方法,但它至少包含了基類的方法。向上轉型給我們帶來的好處就是我們可以將不同的派生通過一種統一的方式進行處理。向上轉型帶來的弊端就是我們向上轉型的過程會丟失派生類的接口。既然有向上轉型,也就有向下轉型即DownCasting我們在此不做詳細討論。下面使用一個例子來示例向上轉型。
public class Base
{
public void Test()
{
MessageBox.Show("OK");
}
}
public class Derive:Base
{}
private void button1_Click(object sender, System.EventArgs e)
{
Base b=new Derive();
b.Test();
}
在有名的OOD的設計原則中有一個叫做里氏代換原則(Liskov Substitution Principle, LSP)。它的實質也就是講向上轉型。它的內容是:任何接收父類型的地方,都應當能夠接收子類型,換句話說如果使用的是一個基類的話,那麼一定適用於其子類,而且程序察覺不出基類對象和子類對象的區別。LSP是繼承複用的基石,只有當派生類可以替換掉基類,軟件的功能不受到影響時,基類才能真正被複用。
2.2 多態
我不敢想象離開了多態後的設計模式是一個什麼樣子。什麼是多態,我喜歡總結這樣一句話來回答這個問題,“一個接口,多種實現”。注意這裏的接口不僅僅表示Interface關鍵字,是廣義上的接口。在C#中實現接口我們有兩種途徑,一種是藉助繼承來實現,一種是藉助Interface來實現。
3、工廠設計模式理論
3.1 概述
工廠模式具體包括了簡單工廠、工廠方法、抽象工廠,它們是按照從簡單到複雜的順序排列的,屬於設計模式中的創建型,其中簡單工廠並不屬於GOF的23中模式。但是它是理解其它的工廠模式的一個很好的基礎,所以很多人在講述設計模式的時候會提到簡單工廠模式。創建型模式關注的是對象的創建,創建型模式將創建對象的過程進行了抽象,也可以理解爲將創建對象的過程進行了封裝,作爲客戶程序僅僅需要去使用對象,而不再關心創建對象過程中的邏輯。
3.2 不使用任何模式
我們現在有這樣的一個設計,影像家電(VideoWiring)包括了DVD、VCD。在基類VideoWiring中有PlayVideo方法,子類重載了這個方法。
我們如何來調用PlayVideo進行播放呢。我們可以看到下面的代碼可以實現。
{
public abstract string PlayVideo();
}
public class VCD: VideoWiring
{
public override string PlayVideo()
{
return "正在播放播放VCD";
}
}
public class DVD: VideoWiring
{
public override string PlayVideo()
{
return "正在播放播放DVD";
}
}
下面是調用對象的方法進行播放的代碼:
dvd.PlayVideo();這樣的語句。
{
DVD dvd=new DVD();
MessageBox.Show(dvd.PlayVideo());
VCD vcd=new VCD();
MessageBox.Show(VCD.PlayVideo());
}
上面的代碼可以實現功能但是不好,爲什麼呢?類實現了多態,但是我們在調用的時候並沒有利用多態。如果我們有很多的影像家電產品,就需要寫很多的類似
下面是使用多態完成播放功能的代碼:
{
VideoWiring vw;
vw=new DVD();
Play(vw);
vw=new VCD();
Play(vw);
}
private void Play(VideoWiring vw)
{
string str=vw.PlayVideo();
MessageBox.Show(str);
}
無論是什麼影像家電產品,我們都可以使用一個統一的方式進行播放,即vw.PlayVideo()。
我們再討論一下,上面的代碼存在的問題。雖然上的代碼很短,應該不會有問題,但是我們定位的目標應該更高些,應該考慮怎樣達到良好的封裝效果,減少錯誤修改的機會。我們自然的應該考慮對象創建的問題了,能不能讓不同的影像家電產品的創建方式相同,而且這個創建過程對使用者封裝,也就是說讓對象的創建象播放功能那樣簡單、統一。如果能夠實現,會給我們的系統帶來更大的可擴展性和儘量少的修改量。“哇!那該多好呀”。“不要羨慕了,來看看簡單工廠模式,聽說它能夠實現”。
3.3 簡單工廠模式
我們使用簡單工廠對上面的代碼繼續改進,根據上面的分析我們考慮對對象創建進行近一步的封裝。使用一個類專門來完成對對象創建的封裝,這個類我們稱爲工廠,因爲它的作用很單一就生成出一個個的類。下面是一個工廠類的示例代碼:
{
public static VideoWiring factory(string VideoName)
{
switch(VideoName)
{
case "DVD":
return new DVD();
case "VCD":
return new VCD();
}
return null;
}
}
這樣我們的客戶端代碼又可以更加有效簡潔了:
注意:在上面的兩段代碼示例中我們就已經使用了向上轉型。首先注意在Create類的factory方法中使用了return new DVD();這樣的語句,但是這個函數的返回值卻是VideoWiring,它DVD類的基類。所以我們的客戶程序纔可以使用VideoWiring vw=Create.factory("DVD")這樣的語句。這樣客戶程序並不關心創建是如何完成的,以及創建的對象是什麼,我們都可以調用基類統一的接口實現他們的功能。使用UML表示如下圖所示:
{
VideoWiring vw=Create.factory("DVD");
vw.PlayVideo();
vw=Create.factory("VCD");
vw.PlayVideo();
}
我們將工廠模式推廣到一般的情況,它的類圖如下所示:
角色說明:
工廠類(Creator):根據業務邏輯創建具體產品,由客戶程序直接調用。
抽象產品(Product):作爲具體產品的基類,提供統一的接口,也是工廠類要返回的類型。
具體產品(Concrete Product):工廠類真正要創建的類型。上圖中僅僅展示了一個具體產品,有多個產品的時候類似。
下面我們對簡單工廠模式進行總結。使用簡單工廠的好處是:1、充分利用了多態性不管什麼具體產品都返回抽象產品。2、充分利用了封裝性,內部產品發生變化時外部使用者不會受到影響。他的缺點是:如果增加了新的產品,就必須得修改工廠(Factory)。抽象工廠模式可以向客戶端提供一個接口,使得客戶端在不必指定產品的具體類型的情況下,創建多個產品族中的產品對象。這就是抽象工廠模式的用意。
3.4 工廠方法
有了簡單工廠模式後,已經給我們帶來了一些好處,但是還存在一些問題,如果我們又多了一個影像家電產品MP4之後,我們可以使MP4類從VideoWiring派生,但是卻要修改Create類的代碼使它能夠生產出MP4這個產品來。不好的地方就在於,我們每次多一個產品的時候都需要修改Create而不是保持原來的代碼不修改僅僅進行一種擴展。在Create類中修改不是每次都簡單的多一個Case語句就能夠解決問題。因爲Create類中還封裝了創建對象的邏輯,有可能還需要修改這些邏輯。這就違反了面向對象設計中一個很重要的原則“開-閉”原則。
“開-閉”原則(the Open Closed Principle OCP):
在面向對象設計中,如何通過很小的設計改變就可以應對設計需求的變化,這是令設計者極爲關注的問題。開閉原則就是一個軟件實體在擴展性方面應該是開放的而在更改性方面應該是封閉的。這個原則說的是,在設計一個模塊的時候,應當使這個模塊可以在不被修改的前提下被擴展。通過擴展已有的軟件系統,可以提供新的行爲,以滿足對軟件的新需求,使變化中的軟件系統有一定的適應性和靈活性。已有的軟件模塊,特別是最重要的抽象層模塊不能再修改,這就使得變化中的軟件系統有一定的穩定性和延續性。因此在進行面向對象設計時要儘量考慮接口封裝機制、抽象機制和多態技術。
前邊設計(簡單工廠)中存在的問題就是它分裝了創建不同對象的邏輯,當有新的產品的時候不易擴展。在開閉原則的指導下我們考慮如何重新修改前邊的設計,我們要儘量使用抽象機制和多態技術。我們放棄對創建不同對象的邏輯的封裝,也採用類似產品的方式,抽象出抽象工廠,具體工廠,具體工廠從抽象工廠派生,每個具體工廠中生產一種具體的產品。“太棒了,告訴你,你的這個想法就是工廠方法模式”。
下面使用工廠方法模式修改前邊的設計:
{
public abstract VideoWiring factory();
}
public class DVDCreate: Create
{
public override VideoWiring factory()
{
return new DVD();
}
}
public class VCDCreate: Create
{
public override VideoWiring factory()
{
return new VCD();
}
}
VideoWiring、DVD、VCD三個類的代碼和前邊的相同,下面我們看看在客戶端如何使用。
下面我們考慮需要擴展一個新的產品MP4的時候如何處理。
{
VideoWiring dvd,vcd;
Create dvdCreate,vcdCreate;
dvdCreate=new DVDCreate();
dvd=dvdCreate.factory();
Play(dvd);
vcdCreate=new VCDCreate();
vcd=vcdCreate.factory();
Play(vcd);
}
我們來看看增加的代碼:
{
public override VideoWiring factory()
{
return new MP4();
}
}
public class MP4: VideoWiring
{
public override string PlayVideo()
{
return "正在播放MP4";
}
}
我們再看看客戶端代碼:
MP4的時候沒有修改原來的代碼,而僅僅是對原來的功能進行擴展系統便有了MP4這個產品的功能。
{
VideoWiring dvd,vcd;
Create dvdCreate,vcdCreate;
dvdCreate=new DVDCreate();
dvd=dvdCreate.factory();
Play(dvd);
vcdCreate=new VCDCreate();
vcd=vcdCreate.factory();
Play(vcd);
//下面是新增的代碼
VideoWiring mp4;
Create mp4Create;
mp4Create=new MP4Create();
mp4=mp4Create.factory();
Play(mp4);
}
我們可以看出使用了工廠方法模式後,很好的滿足了開閉原則,當我們增加了一個新的產品
將工廠方法模式推廣到一般情況:
角色說明:
抽象工廠(Creator):定義具體工廠的接口,所有的創建對象的工廠類都必須實現這些接口。
具體工廠(ConcreteCreator):具體工廠包含與應用密切相關的邏輯。複雜創建具體的產品。
抽象產品(Product):所有產品的基類。
具體產品(ConcreteProduct):實現抽象產品申明的接口。工廠方法模式所創建的每個對象都是某個具體產品的實例。
工廠方法模式的用意是定義一個創建產品對象的工廠接口,將實際創建工作推遲到子類中。工廠方法模式是簡單工廠模式的進一步抽象和推廣。由於使用了多態性,工廠方法模式保持了簡單工廠模式的優點,而且克服了它的缺點。在工廠方法模式中,核心的工廠類不再負責所有的產品的創建,而是將具體創建的工作交給子類去做。這個核心類則成爲了一個抽象工廠角色,僅負責給出具體工廠子類必須實現的接口,而不接觸哪一個產品類應當被實例化這種細節。這種進一步抽象化的結果,使這種工廠方法模式可以用來允許系統在不修改具體工廠角色的情況下引進新的產品。
3.5 抽象工廠模式
我們繼續對影像家電產品的情形進行分析,我們已經可以使用工廠方法比較好的實現了產品的創建,但是在以前的分析中我們並沒有考慮產品種類及生產廠家這樣的問題。就拿DVD來說TCL可以生產、LG也生產等等很多廠家都生產。DVD是產品種類中的一種,產品種類這個概念在有些書上稱爲產品族。從另外一個角度來看TCL可以生產DVD、VCD等等很多產品,這些產品在一起就可以構成一個產品結構。當我們考慮了這些問題後,提出了兩個概念:產品種類、產品結構。我們在工廠方法中討論的是一個個單一的產品的創建,如果我們對這個問題進行進一步的研究、拓展,就應該從單一的產品過度到多個產品種類,在工廠方法中我們考慮DVD是一個單一的產品,現在我們認爲DVD是一個產品種類,有TCL生產的DVD,有LG生產的DVD,VCD是另一個產品種類,有TCL生產的VCD,有LG生產的VCD。就這個問題我們重新分析,有兩個產品種類分別是DVD、VCD,有兩個工廠是TCL和LG,它們分別生產DVD和VCD。我們使用下面的類圖來表示:
DVD是抽象類它提供統一的接口,LGDVD、TCLDVD是兩個具體的類。VCD和DVD類似。有一個抽象的工廠Create,從它派生了兩個具體的類TCLCreate、LGCreate。Create中提供了兩個抽象方法factoryDVD和factoryVCD它們提供了兩個接口,用於創建DVD產品和VCD產品。在TCLCreate、LGCreate中實現這兩個方法。這樣TCLCreate就可以創建自己的DVD、VCD,同樣LGCreate也可以傳經自己的產品。
下面是代碼結構:
public abstract class Create
{
public abstract DVD factoryDVD();
public abstract VCD factoryVCD();
}
public class LGCreate: Create
{
public override DVD factoryDVD()
{
return new LGDVD();
}
public override VCD factoryVCD()
{
return new LGVCD();
}
}
public class TCLCreate: Create
{
public override DVD factoryDVD()
{
return new TCLDVD();
}
public override VCD factoryVCD()
{
return new TCLVCD();
}
}
public abstract class DVD
{
public abstract string PlayVideo();
}
public class LGDVD: DVD
{
public override string PlayVideo()
{
return "LG的DVD在播放";
}
}
public class TCLDVD: DVD
{
public override string PlayVideo()
{
return "TCL的DVD正在播放";
}
}
public abstract class VCD
{
public abstract string PlayVideo();
}
public class LGVCD: VCD
{
public override string PlayVideo()
{
return "LG的VCD正在播放";
}
}
public class TCLVCD: VCD
{
public override string PlayVideo()
{
return "TCL的VCD正在播放";
}
}
客戶端使用抽象工廠代碼如下:
private void button1_Click(object sender, System.EventArgs e)
{
Create TCL,LG;
TCL=new TCLCreate();
LG=new LGCreate();
PlayDVD(TCL); //輸出“TCL的DVD在播放”
PlayDVD(LG); //輸出“LG的DVD在播放”
}
private void PlayDVD(Create create)
{
DVD dvd=create.factoryDVD();
MessageBox.Show(dvd.PlayVideo());
}
下面將抽象工廠模式推廣到一般情況,它的類圖如下所示:
抽象工廠:提供所有具體工廠的接口,與應用系統的具體商業邏輯無關。基本上爲每一個產品種類提供一個創建方法。
具體工廠:具體負責創建產品結構中每個產品。它包含了創建不同產品的商業邏輯。它實現抽象工廠中的接口。
抽象產品:定義產品的共同接口。
具體產品:是客戶需要創建的具體對象。
在工廠方法中每個工廠負責創建一個產品,在抽象工廠中每個工廠創建一系列產品。在上面舉例中使用TCL、LG這樣的實際的工廠,在實際的應用中,往往是我們根據產品抽象了類,它們主要負責一系列產品的創建,將這些負責抽象工廠的類稱爲具體工廠,從這些具體工廠更進一步進行抽象,抽象出的工廠稱爲抽象工廠。下面我們看看抽象工廠模式的擴展。
抽象工廠的擴展包括了新增一個產品種類及新增一個工廠。當在現有的抽象產品下添加新的具體產品時我們僅僅需要增加一個新的工廠就可以。比如現在有了Haier(海爾)的DVD及VCD,我們很容易就實現擴展,而且也滿足了“開閉原則”。如下圖所示:
當我們有了一個新的產品的增加的時候,就不能很好的滿足“開閉原則”了,因爲我們需要修改每個產出的方法從而是現有的工廠可以創建出新的產品。比如我們增加一個Mp4產品種類。