1. 程式人生 > >【GOF設計模式之路】-- Factory

【GOF設計模式之路】-- Factory

自從開始工作,就感覺精力相比在大學時有很大幅度的下降。大二那一年精力最旺盛,自從大二結束開始工作到現在,兩年時間,似乎精力都已經不受自己控制了。如果對一些技術研究工作不是很感興趣,下班之後基本上到晚上10點左右就想睡覺。工作兩年加上大二的一年,一直到現在都堅持每天必須有新的東西進入腦子,進步倒是明顯感受到了,但真擔心現在的精力還能堅持幾年的技術研究。但願不要像大家說的到了30歲以後就不適合做技術研究了,我個人覺得人活著就是為了做自己喜歡的事,那麼就等到自己不再喜歡技術研究時再考慮轉型吧。這個過程可能是一輩子,也可能是短短的幾年,因人而異吧,先走好當前的路!come on!!

不知不覺距寫前一篇Singleton時已經有一個多月了,一是忙,二是精力不足,三是加上煩心的事就完全沒心情寫博了。有時候一直在想人活著是為了啥,似乎沒有明確的答案。可能我還需要磨練吧。前一篇Singleton,就有一些朋友說寫得比較複雜,在實際中基本不會搞得那麼複雜,我之前也說過,設計模式並不需要嚴格遵循,可以根據實際情況做一些具體特殊的優化和演化,所以複雜的情況是應付複雜的需要,簡單的是為了簡單的需要。我們也沒有必要為了簡單的需要而使用複雜的規則,反之亦然。也就是所謂的靈活應對吧,更何況本系列的設計模式的示例是用C++解釋的。更好玩的是有朋友的評論Singleton:“這是我見到過寫得最好的Singleton”,這讓我倍感欣慰,可這時又有朋友回覆這個朋友的評論說:“是寫得最多才對!”。欣慰之下又多了一些反思,難到我寫的Singleton真的沒有被參考的價值?其實,反過來想,我寫博並不是為了最好最牛,只是習慣性寫寫,對自己有幫助,做到自己的最好即可。當然既然寫出來了,也儘量在能力範圍內不誤導別人,也非常樂意接受別人批評。批評對於我來說是一面很不錯的鏡子。

好了,迴歸正題,本篇介紹工廠模式(Factory),在本文中,會介紹三種工廠模式相關的設計,即:(簡單工廠)Simple Factory、(工廠方法)Factory Method(抽象工廠)Abstract Factory。雖然Simple Factory不是GOF的成員,但它在實際中也很常見和實用,通常作為Factory的一種特殊模式。本系列是以GOF設計模式為標題,但並不表示所有的模式都得在GOF的範圍內,也即所謂的靈活吧!

簡單工廠(Simple Factory)

還是以最簡單實用的簡單工廠開始,也好逐步加深印象,同時便於理解。所謂簡單工廠(Simple Factory),自然也就是簡單設計為主,直截了當。舉個例子,例如網路遊戲,在遊戲世界裡有很多個物件(Object),例如玩家(Player)、NPC、怪物(Monster)等。這些物件就好比一個個的產品,它們屬於一個產品大類。而這一個個的產品又是由某個工廠所製造出來的,這裡的工廠就可以是物件管理器。我們使用者在想要建立一個物件時,只需要告訴物件管理器(工廠)我們需要什麼型別的物件(產品),然後就把建立(生產)物件(產品)的任務託付給了物件管理器(工廠)。而我們只需要使用建立(生產)出來的物件(產品),專注於其商業邏輯。

由此我們可以構建一個簡單工廠,可以有兩種形式,一是每一種物件都使用一個建立函式,二是所有物件都使用同一個建立函式,由引數區分建立的物件的種類。在實際中,往往偏向第二種形式。示例程式碼如下:

呼叫如下:

如上,IObject即是物件(產品)基類,所有物件(產品)都繼承於它。CPlayer、CNpc和CMonster則是具體的物件(產品)類。CObjectManager則為物件管理器(簡單工廠),它負責建立(生產)CPlayer、CNpc和CMonster等具體的物件(產品)。在上例中,我使用了IID這種形式來確定建立什麼類的實體,IID可以理解為物件唯一ID。物件管理器在建立物件時可以根據唯一ID進行區分,如main函式中的pObj1和pObj2,另外,在很多場合都是使用的基類指標進行邏輯處理,在特定的時候會需要動態轉換以確定這個IObject*是什麼子型別(當然設計上不推薦這麼做)。因此在IObject類裡增加了DynamicCast函式,此函式可根據引數的IID值,返回具體的子類,如果沒有找到則返回NULL。VCAST是一個虛擬函式,是為了向下定位查詢子類是否有IID與其引數的IID相同,若沒有則返回NULL(如上例中的DynamicCast( IID_MONSTER )則會失敗而返回NULL)。在上例中,IObject只被繼承了一層,所以當nIID與子類的IID匹配時(如CPlayer對應IID_PLAYER),m_iIID == iIID始終是成立的,VCAST也就不會再呼叫,顯得作用不大,但是當有多層繼承時,而IObject類只能儲存起子類繼承關係中某一層的IID,此時,m_iIID == iIID就不一定成立了,此時VCAST的作用就明顯了,例如CPlayer還有子類,則CPlayer的子類的VCAST應該設計為:

virtual IObject* VCAST( const int iIID ){ return ( iIID == IID_PLAYER || iIID == IID_SUBPLAYER ) ? this : IObject::VCAST( iIID ); 

因為有多層繼承關係時,可允許子類不重新修改IObject的m_iIID的值,所以判斷了本身類的IID和基類的IID。當IID不等時,則直接呼叫IObject::VCAST( iIID )返回到IObject基類中,將由它統一決定返回值,本文中統一返回NULL。(VCAST函式的大部分邏輯都是一樣的,可以考慮使用巨集定義)

其實這個DynamicCast和VCAST組合也就起到了dynamic_cast的作用,dynamic_cast在效率上要低一些,它是通過RTTI描述符進行定位,而DynamicCast通過VCAST虛擬函式定位子類的VCAST。在編譯時就已經決定了呼叫DynamicCast時該呼叫虛擬函式表裡哪個虛擬函式,dynamic_cast的轉換是具體存在的。(PS:在實際中並不推薦使用dynamic_cast和DynamicCast,在抽象一層應該做好介面,避免直接面對具體的物件型別)

如果對這個流程不清楚,可以自己寫寫再跟蹤一下,就能有所體會了。

如果將物件的建立使用不同的建立函式,如上面所說的第一種形式,則CObjectManager可以設計為:

這樣在使用時,就只能分開建立了,我個人偏好傳遞引數的方式進行建立。

Simple Factory有時有稱作靜態工廠,靜態工廠也是簡單工廠,與上面示例的不同之處在於工廠類的建立函式是靜態的,以至於在建立物件時,可以不用建立工廠物件,如下:

這種方式在實際的複雜的繼承情況中可能並不適用,它將建立函式設計為靜態,便不能讓其子類重寫和擴充套件了。

上例的輸出結果為:

Player Run
Monster Run
Player Run

最後一次DynamicCast( IID_MONSTER )失敗了,返回NULL,不會輸出。

為了更直觀的總結一下簡單工廠模式,如下圖:

圖1   簡單工廠模式(Simple Factory)

好了,簡單工廠也就差不多了,小結一下:

其優點:

一是將工廠中的各個產品的建立都集中到一起,便於管理與維護,能夠減少以後修改的工作量。二是將主體邏輯與抽象邏輯分離,工廠負責抽象及創建出產品,使用者則只需要負責主體邏輯,分工明確且非常協調。(這兩點也可歸結於整個工廠模式的好處)

其缺點:

對未來的擴充套件性適應不是非常良好,它對修改不封閉,即如果要新增加一個產品,則需要修改工廠的實現,這違反了開閉原則(OCP)。

工廠方法(Factory Method)

前面談到Simple Factory的缺點時,發現它違反了開閉原則,每增加一個產品就得修改工廠建立函式的邏輯實現。例如要增加一個新的物件CItem(遊戲中的道具),此時就得修改CreateObject函式,以後每增加一個物件都得這樣做,CObjectManager就不能對修改封閉。

那麼,如何才能解決這個問題呢,想想面向物件,想想多型,於是Factory Method模式應運而生。我們可以將工廠抽象了,然後再繼承一系列的子工廠。例如某某集團公司,這個集團的名稱可以只是掛一個名,而此集團下可以有很多個子公司,每個子公司就負責做具體的實事。而集團總部就只需要支配即可。每個子公司的人事、財政等運作都是獨立的,互不干涉。假如要將集團戰略發展到一個新的領域,只需要新建一個子公司或購買一個公司作為子公司。這也就是所謂的修改封閉。再例如某餐廳,最開始是隻請了一個廚師(簡單工廠),這個廚師只會做川菜,對於此刻餐廳的規模來說,已經完全足夠了。隨著這名廚師的廚藝被大家認可之後,餐廳的生意也越來越好了。需求也不斷增加,更有外地的顧客光臨,餐廳為了能夠讓外地的顧客能夠嚐到家鄉的味道,於是“做出了一個艱難的決定”,要讓廚師學習做異地菜餚,如魯菜。可是廚師師傅又要下廚,又要學習,他都一把年紀了,哪兒有那麼多精力呢,更何況這是一個熟能生巧的活。餐廳老闆也能體會廚師師傅的苦,於是提升他為廚師總管(沒辦法,他資格最老嘛),然後又招聘了很多個廚師,有川菜廚師、魯菜廚師、湘菜廚師等等。而廚師總管以後就不用下廚了,他只需要直接面對餐廳老闆,下達命令。這樣既形成培養模式,又能不讓廚師總管學習做各種地域的菜餚了。需要新的地域菜餚就直接招聘新的廚師,而原來的廚師也不需要涉足太廣。於是餐廳的規模也就越做越大了,老闆成了億萬富翁。。。

瞭解了整體結構和流程之後,Factory Method和Simple Factory的區別其實不明顯,唯一的區別只是將工廠抽象化了,然後再建立一系列的子工廠。CreateObject還是同樣的邏輯,只是此刻的CreateObject不能為static了,它要被子工廠重寫。還是先看程式碼吧,如下:

抽象工廠及子工廠:

 

使用者:

如上,我們將原先的CObjectManager提升(抽象)為集團公司(抽象工廠):IObjectManager。而CPlayer、CNpc和CMonster還是作為同一個物件管理器(工廠)的物件(產品),由CFightObjManger(可戰鬥物件管理器)管理和建立(生產)。新增加的兩個物件(產品):CItem(道具)和CBuilding(建築物)則由新的CRegionObjManager(場景物件管理器)管理和建立(生產)。以後再增加新的物件則不需要改變CFightObjManger和CRegionObjManager管理器(工廠)。實現了修改封閉,符合OCP。此時,你可能發現有一個問題,CFightObjManger和CRegionObjManager明顯是兩大類,意思就是如果將CItem和CBuilding放到CFightObjManager裡並不合適,反過來將CPlayer、CNpc和CMonster放到CRegionObjManager裡建立也不怎麼合適。這樣就又可能出現違反OCP的情況,即如果CFightObjManger和CRegionObjManager已經存在,而新增的物件(產品)在型別上是應該屬於它們兩者中某一個工廠的,此刻建立新的管理器(工廠)就顯得沒必要。此時就還是得修改CFightObjManger或CRegionObjManager的實現。因此,要儘量解決這個問題,就得在設計工廠時,儘量把以後需要的產品都想到最全面,減少修改的次數,儘量實現修改封閉(PS:所以架構師也不是那麼好做的-.-)。你總不可能在實際研發中把各個工廠都命名為Factory1,Factory2...喲!-_-!!!

這樣看來,具體的工廠子類要儘量不被修改就得看設計者的思維了,而Factory Method對於頂層的抽象工廠(IObjectManager)來講,是符合OCP的,它不負責具體實現,只需要傳送命令,好比前面的集團公司總部和廚師總管,他們完全可以“坐吃山空”。

上面的程式碼很簡單,就不具體解釋了,輸出結果為:

NPC Run
Building Run

Factory Method還可以做進一步演化,可以將所有的產品物件聚集在一起,例如有一個Object容器,當用戶需要建立新的Object時,首先到容器裡查詢是否已經存在,如果存在則直接返回,不存在則建立一個此型別的Object,然後加到容器裡。這樣便能夠迴圈利用,這也就是享元模式的特色。以後待談及到享元模式時再具體討論吧。在實際中,通常是多種模式相結合,已達到程式的需求。

工廠方法(Factory Method)的流程圖示如下:

 

圖2   工廠方法模式(Factory Method)

工廠方法就差不多這麼多了,小結一下:

除了擁有簡單工廠的優點之外,還彌補了簡單工廠的OCP問題,各個工廠相對獨立,在實際中可以確定為不同的工廠型別。這樣也更符合實際,想想既然產品都可以各種型別,工廠自然也可以有各種型別了。

另外,在上面的簡單工廠和工廠方法裡,在使用者使用工廠時,應該依耐於抽象層,而不應該依耐於具體的工廠,對於產品也一樣,應該依耐於抽象產品程式設計,而不是具體的產品,如果依耐於具體的產品就失去了工廠的意義和多型的意義。

抽象工廠(Abstract Factory)

前面談及CFightObjManger或CRegionObjManager時,談到可能違反OCP的那種情況,也正好有了抽象工廠模式的影子,抽象工廠模式說專業一點就是解決產品族和產品等級結構之間的關係問題的。關於產品族和產品等級可以舉個例子,如:遊戲中的怪物和物品,通常情況下,副本中的怪物要比野外的怪物強(假設怪物也分為副本怪物和野外怪物),副本中的物品也比野外怪物爆出的物品強(假設物品也分副本和野外)。那麼副本中的怪物和物品屬於同一個產品族,野外的怪物和物品屬於同一個產品族。而從縱向看,怪物屬於一個產品等級,物品屬於一個產品等級。如下圖:

圖3   產品族和產品等級

從圖3中可知,之前的工廠方法是生產的同一個產品等級的產品,它們擁有共同的抽象基類。也就是同一系列的產品,例如CItem系列、CBuilding系列、CPlayer系列等。而Abstract Factory模式則是要建立同一個產品族的產品,例如副本產品和野外產品。同一個產品族通常不是同一系列的產品,因此, Abstract Factory包含多個產品的建立方法,從而又出現了OCP問題,當在一個產品族裡增加一個新的產品時,對修改不封閉,也就是對增加產品等級的修改不封閉。只對增加一個產品族的修改封閉。這種情況也是必然,正所謂魚和熊掌不可兼得。

再例如,我們常用的介面UI控制元件Button和Edit,為了表達多個平臺下的介面,可分為windows、mac和unix等。於是有了WinButton、MacButton和UinxButton,WinEdit、MacEdit和UnixEdit。那麼Button和Edit則分別處於兩個不同的產品等級,產品族則有3個,因為有3種平臺。如下圖:

圖4  產品等級與產品族示例

由此看來,產品族就好比將不同的產品進行捆綁式的生產,以達到特定的需求。好了,直接貼程式碼吧,如下:

以物品和怪物為例,我們將CItem和CMonster作為具體產品的基類,也可以進一步抽象,可視情況而定:

然後,設計抽象工廠:

使用者:

如上,CFBObjManager和CFieldObjManager分別屬於兩個產品族,它們都擁有兩個產品等級CItem和CMonster。並且IObjectManager此時有兩個建立函式CreateMonster和CreateItem,它們返回的是具體產品族裡的產品基類指標。

如此一來,我們便可以通過抽象工廠建立不同的產品族,例如上面的副本產品和野外產品,分別是由副本工廠和野外工廠負責生產。同樣,前面聊到的餐廳,雖然規模大了,但是某天有位外地顧客突然想吃燒白。而當前只有四川風味的燒白,對於外地人可能不是很適應,於是老闆下令各個地域菜的廚師都得學會做燒白,這樣便能做出湘燒白、魯燒白和粵燒白等。這樣就能讓外地顧客更加喜歡光臨此餐廳了。但是對於餐廳來說,每個廚師都得學習燒白的做法,燒白相當於增加了一個產品等級,燒白將納入各個地域菜產品族裡。因此可謂是大動干戈啊,廚師師傅們有點小情緒,因為得學習啊,產品等級對修改不封閉。

好了,上面程式的輸出結果為:

FB Monster Run
FB Item Run
Field Monster Run
Field Item Run

Abstract Factory的流程圖示如下:

圖5   抽象工廠模式(Abstract Factory) 

小結一下:

抽象工廠模式和工廠方法模式的結構差別不是很大,可以將工廠方法模式看著是抽象工廠模式的一種特殊情況,而抽象工廠模式也可看著是工廠方法模式的擴充套件推廣。其實這之間的微妙關係在實際中能夠得以體現,並且可結合使用,靈活調整。

適用性總結:

簡單工廠(Simple Factory):

當一個類不知道它所必須建立的物件的類的時候。

當一個類只需要簡單指定需要建立的物件的時候。

當所有產品都打算集中在某一個建立類裡德時候。

工廠方法(Factory Method):

當一個類不知道它所必須建立的物件的類的時候。

當一個類希望由它的子類來指定它所建立的物件的時候。

當類將建立物件的職責委託給多個幫助子類中的某一個,並且你希望將哪一個幫助子類是代理者這一資訊區域性化的時候。

抽象工廠(Abstract Factory):

一個系統要獨立於它的產品的建立、組合和表示細節,這點對所有工廠模式都很重要。

一個系統要由多個產品系列中的一個來配置時。

當你要強調一系列相關的產品物件的設計以便進行聯合使用時。

當你提供一個產品類庫,而只想顯示它們的介面而不是實現時。

總結:

工廠模式的工廠的本質即是將建立(生產)集中化管理。產品有問題可以直接找工廠,同時在某些模式上做到對修改封閉,減少了工作量,並且更大程度上做到了複用性。再者,有了工廠,使用者則不需要再管理產品的生產過程,而直接關注業務邏輯,分工明確,結構清晰。工廠模式都是以抽象的形式構建,使用者介面也獲得了更好的通用性。

PS:本文只是談及了3個工廠的基本框架,在實際中可以靈活調整以供需求之用。本文的圖形示例都是按照我自己的理解進行繪製的,它們看起來沒有UML那麼專業,我始終喜歡以一種通俗的方式去理解我看到的事物。如果你覺得這些圖示不夠清晰,就請參見網路上其它地方的工廠模式的UML圖例吧。本文的程式碼只作為示範之用,在實際中往往要複雜很多,本文只是為了闡述三種工廠的基本框架。更多更好的設計還望大家指出,在此作為拋磚引玉吧。

出於水平能力問題,可能存在疏漏或錯誤,還望大家提出,非常感謝!本文到此結束!

 

【GOF設計模式之路】目錄