GoF的設計模式一共23個,可以分為3大類:創建型、結構型和行為型,這篇文章主要討論創建型。
創建型的設計模式包括:簡單工廠(Simple Factory)、工廠方法(Factory Method)、抽象工廠(Abstract Factory)、單例(Singleton)、構造者(Builder)和原型(Prototype),我們分別來討論。
我們首先來看工廠系列的3個設計模式,它們都主要是針對軟件設計中的“開放-封閉”原則,即程序應該對擴展開放,對修改封閉。特別是當我們的程序采用XML+反射的方式來創建對象時,工廠模式的威力就完全展現出來了,這時我們可以通過維護配置文件的方式,來控制程序的邏輯。
1)簡單工廠,當我們的程序在實例化對象時,如果輸入條件不一樣,產生的對象也不一樣,那么我們可以考慮使用簡單工廠對不同的實例進行統一封裝, UML結構如下:
優點:封裝了具體對象的實例化過程,Client端和具體對象解耦,同時ProductManager可以作成靜態類或者Singleton對象,然后可以使用HashMap緩存具體對象(前提是對象沒有時間依賴性),降低創建對象的次數。
缺點:當增添一種新類型的對象時,需要修改Productmanager的代碼(如果不采用XML)
2)工廠方法,它是針對簡單工廠的改進版,添加了對ProductManager的抽象,UML結構如下:
優點:結構更加靈活,對于某種類型的對象來說,會有一個特定的對象工廠指向它,這樣當我們需要添加一種新類型的產品時,只需要添加兩個類,一個是具體產品類,一個是新產品的工廠類。這樣更加靈活。
缺點:結構開始變得復雜,而且最終還是需要Client端來確定究竟使用哪一個Factory(當然這個信息可以保存在上下文或者配置文件中)。
3)抽象工廠,這個是最復雜的工廠模式,它用來生成一個產品線上的所有產品,我們假設一個產品線上包括多個產品,不同的產品線上的產品個數是一樣的,這樣我們需要一個針對產品線的抽象,并且很顯然不同產品線上的產品是不可能混到一起的。對應的UML結構圖如下:
上圖表明,一個產品線上的產品由IProduct1和IProduct2組成,客戶端在獲取產品時,這兩個產品應該是同時返回的,因此對于IProductManager來說,它需要同時生成這兩個對象。
優點:對創建產品家族的行為高度抽象,添加一個產品線的邏輯比較清晰。
缺點:當我們對產品線上的產品進行增加和刪除時,對應的操作比較麻煩,所有的產品工廠都需要進行修改。
4)單例,這是比較好理解的一個模式,從字面上說,就是程序在運行的過程中,希望在任意時刻,都只保留某個對象的唯一實例。對應的UML結構圖如下:
單例的實現方式一般包括幾步:1)私有的指向自身的字段;2)私有構造函數;3)公開對私有字段進行實例化的方法。也有幾種針對具體語言進行的改善,例如針對多線程采用double lock機制,采用常量方式定義私有字段、使用內嵌類來實例化字段等。
我們也可以對單例進行一些適當的擴展,例如我們將對象的個數由1個變為N個,這就成了對象池。
通常工廠模式中會使用到單例模式,特別是對于簡單工廠來說。
5)構造者,對于一些復雜對象來說,它可以分成多個不同的部分,在實例化時,不同部分之間實例化的順序,有時會有嚴格的限制,這時我們就可以使用構造者模式了。對應的UML結構圖如下:
我們定義了IBuilder接口來實例化對應的不同部分,同時有一個方法來返回對象的實例。而Constructor類的Construct方法會按照業務邏輯依次調用實例化部分對象的方法,即BuildPartA、BuildPartB,這里的調用順序,完全由業務邏輯來控制,最后可以調用GetProduct方法取得完整的對象實例。
我們有時也會對上圖進行修改,例如將GetProduct放到Constructor中,或者將Construct方法放入到GetProduct(取消Constructor)中。即使有這些變形,但是基本的思想是不變的。
6)原型,我們在程序運行過程中,當需要有新的實例對象時,有時并不希望是從頭創建一個對象,而是希望新的實例的狀態和某個已存在的實例保持一致,這就是原型模式發揮作用的地方。對應的UML結構圖如下:
在.NET中,已經定義了IClonable接口來實現原型模式。需要注意在實現時,會有深拷貝和淺拷貝的區別,深拷貝會同時拷貝堆棧和堆上的內容,而淺拷貝只會拷貝堆棧上的內容。
在這部分里,我們關注GoF里面的結構型模式,它主要是用于描述如何將類組合在一起去構成更大的結構。結構型模式包括適配器(Adapter)、裝飾(Decorator)、橋接器(Bridge)、享元(FlyWeight)、門面(Facade)、合成(Composite)以及代理(Proxy)模式。
下面我們對上面提到的模式分別進行描述。
1)適配器(Adapter)。當我們已經開發出一個模塊,有一套清晰的接口,并且模塊正在被某個功能使用(意味著模塊接口改變的可能性不高),這是如果有另外一個功能也需要使用這個模塊的功能,但是對應的是一套完全不同的接口,這時適配器就可以發揮作用了。
適配器模式分為兩種,一種是對象適配器,一種是類適配器,對象適配器的UML圖如下:
這里Adaptee1和Adaptee2指兩套不同的子系統,它們作為Adapter的屬性存在,可以使用IoC的方式指定。
類適配器的UML圖如下:
同樣是兩個不同的子系統,但是這里我們創建了2個Adapter類來分別指向兩個子系統。在這里我們可以在Client和ITarget之間,設置一個Adapter工廠,來根據業務需求創建不同的Adpater實例。
2)裝飾(Decorator),假如我們已經開發了一套功能,然后根據需求,需要增加一些子功能,而且這些子功能是比較分散比較時可以增刪的,這時如果直接修改接口,那么會造成接口功能復雜并且不穩定,針對這種情況,我們可以使用裝飾模式。對應的UML圖如下:
上圖中,ConcreteComponent已經實現了Component的基本功能,對于一些附加的功能,如果放在ConcreteComponent中不合適的話,我們可以像ConcreteDecoratorA一樣,創建一個基于Decorator的類,通過SetComponent方法將核心功能和輔助功能串在一起。
有時,為了簡單,我們也可以把ConcreteDecorator直接掛在Concretecomponent下面。
3)橋接器(Bridge),面向對象提倡的幾個最佳實踐包括:1)封裝變化;2)面向接口編程;3)組合優于繼承;4)類的職責盡量單一。橋接器完美的體現了這些,通過創建型模式,我們可以很好地達到面向接口編程的目標,也就是說我們在程序中各變量的聲明類型是接口類型或者抽象類,而具體的實現類型則由不同的設計模式使用不同方式指定。這在接口或者抽象類基本穩定的情況下,是很好地,但當接口需要發生變化時,我們如何去處理?可以看看橋接器的UML圖:
通過這個圖,我們可以看出,Implementor接口的變化,對于Client來說,基本是沒有影響的。Abstraction會持有Implementor的一個實例。
4)享元(FlyWeight),當我們系統中需要使用大量的小對象,但我們又不希望將所有的小對象都創建出來時,可以考慮使用享元模式,它會抽取小對象中的公共部分,將其封裝為基類,然后針對不同條件創建小對象,同時在對象池中維護這些小對象,客戶在需要使用小對象時,首先在對象池中查找,如果存在,直接返回。對于小對象中“個性”的部分,由調用小對象的客戶端進行維護。對應的UML圖如下:
除了上述的簡單享元,還存在一種復合享元,對應的UML圖如下:
圖中,CompositeConcreteComponent是不共享的,但是它里面包含很多簡單的享元,這些享元是共享的,我們可以把它想象成一個特殊的“享元工廠”。
通常提到享元,最常見的例子就是文本編輯器中的26個字母,在.NET中,字符串常量也使用了享元模式。
在享元模式中,我們通常會將FlyWeightFactory設計為單例模式,否則享元就沒有意義了。
5)門面(Facade),如果我們的程序需要深入調用某個模塊的內部,但我們又不想和模塊過緊耦合,這時可以考慮使用門面模式,來對外部封裝內部子系統的實現。簡單的門面可能和代理在某種程度上很相似。
門面模式沒有固定的UML圖,它是根據客戶端的實際需求以及子系統內部的接口來確定的。
6)合成(Composite),當我們的對象結構中存在“父子”關系時,可以考慮使用合成模式。它分為兩種,一種是安全型的合成模式,UML圖如下:
這種類型的合成模式,對于Component的增、刪、改,都在Composite中維護,Leaf根本不知道這些操作。另一種是透明型的合成模式,UML圖如下:
這種類型的合成模式,自上而下所有的Component都會有增、刪、改的操作,只不過對于Leaf來說,這些操作時沒有意義的。
7)代理(Proxy),在編寫程序時,有時我們希望使用某個對象或者模塊的功能,但是因為種種原因,我們不能直接訪問,這時就可以考慮使用代理,對應的UML圖如下:
需要注意的是,在這里RealSubject只有一個,如果有多個,那么就是Adapter了。另外,代理也可以加入自己的一些邏輯處理,例如PreExecute和PostExecute。如果這里有多個Proxy,那么就是Decorator了。