天天看點

【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

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

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

【GOF設計模式之路】-- 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)的流程圖示如下:

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

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

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

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

抽象工廠(Abstract Factory)

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

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

從圖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種平台。如下圖:

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

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

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

然後,設計抽象工廠:

如上,CFBObjManager和CFieldObjManager分别屬于兩個産品族,它們都擁有兩個産品等級CItem和CMonster。并且IObjectManager此時有兩個建立函數CreateMonster和CreateItem,它們傳回的是具體産品族裡的産品基類指針。

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

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

FB Monster Run

FB Item Run

Field Monster Run

Field Item Run

Abstract Factory的流程圖示如下:

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

小結一下:

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

适用性總結:

簡單工廠(Simple Factory):

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

當一個類隻需要簡單指定需要建立的對象的時候。

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

工廠方法(Factory Method):

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

當類将建立對象的職責委托給多個幫助子類中的某一個,并且你希望将哪一個幫助子類是代理者這一資訊局部化的時候。

抽象工廠(Abstract Factory):

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

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

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

當你提供一個産品類庫,而隻想顯示它們的接口而不是實作時。

總結:

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

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

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