這裏將介紹到cocos2d-x常用設計模式之二---二段構建模式,顧名思義,其實也就是在創建一個對象時,將內存分配和對象初始化分開進行,這樣做的好處主要是:1.對象在初始化時可能會發生某些異常,C++中,類的構造函數是無返回值的,這樣我們就不得不在可能發生異常的地方藉助try catch來處理,但這種方式會使程序的二進制文件增大不少,而通過二段式構建,就能通過第二段initxxx函數來初始化,通過initxxx函數的布爾返回值進行異常處理;2.C++中,構造函數是不能調用虛函數的,而二段構建模式中我們就可以通過在initxxx函數內調用虛函數,從而實現在創建對象時藉助虛函數來完成一些定製的功能。
【轉載自子龍山人】:http://zilongshanren.com/blog/2012/09/26/cocos2d-x-design-pattern2-two-stage-create/
乍一看標題,大家可能會覺得很奇怪,神馬是“二段構建模式”呢?
所謂二段構建,就是指創建對象時不是直接通過構建函數來分配內存並完成初始化操作。取而代之的是,構造函數只負責分配內存,而初始化的工作則由一些名爲initXXX的成員方法來完成。然後再定義一些靜態類方法把這兩個階段組合起來,完成最終對象的構建。因爲在《Cocoa設計模式》一書中,把此慣用法稱之爲“Two Stage Creation”,即“二段構建”。因爲此模式在cocos2d裏面被廣泛使用,所以把該模式也引入過來了。
1.應用場景:
二段構建在cocos2d-x裏面隨處可見,自從2.0版本以後,所有的二段構建方法的簽名都改成create了。這樣做的好處是一方面統一接口,方便記憶,另一方面是以前的類似Cocoa的命名規範不適用c++,容易引起歧義。下面以CCSprite爲類,來具體闡述二段構建的過程,請看下列代碼:
//此方法現在已經不推薦使用了,將來可能會刪除 CCSprite* CCSprite::spriteWithFile(const char *pszFileName) { return CCSprite::create(pszFileName); } CCSprite* CCSprite::create(const char *pszFileName) { CCSprite *pobSprite = new CCSprite(); //1.第一階段,分配內存 if (pobSprite && pobSprite->initWithFile(pszFileName)) //2.第二階段,初始化 { pobSprite->autorelease(); //!!!額外做了內存管理的工作。 return pobSprite; } CC_SAFE_DELETE(pobSprite); return NULL; }
如上面代碼中的註釋所示,創建一個sprite明顯被分爲兩個步驟:1.使用new來創建內存;2.使用initXXX方法來完成初始化。
因爲CCSprite的構造函數也有初始化的功能,所以,我們再來看看CCSprite的構建函數實現:
CCSprite::CCSprite(void) : m_pobTexture(NULL) , m_bShouldBeHidden(false) { }
很明顯,這個構建函數所做的初始化工作非常有限,僅僅是在初始化列表裏面初始化了m_pobTexture和m_bShouldBeHidden兩個變量。實際的初始化工作大部分都放在initXXX系列方法中,大家可以動手去查看源代碼。
2.分析爲什麼要使用此模式?
這種二段構建對於C++程序員來說,其實有點彆扭。因爲c++的構造函數在設計之初就是用來分配內存+初始化對象的。如果再搞個二段構建,實則是多此一舉。但是,在objective-c裏面是沒有構造函數這一說的,所以,在Cocoa的編程世界裏,二段構建被廣泛採用。而cocos2d-x當初是從cocos2d-iphone移植過來了,爲了保持最大限度的代碼一致性,所以保留了這種二段構建方式。這樣可以方便移植cocos2d-iphone的遊戲,同時也方便cocos2d-iphone的程序員快速上手cocos2d-x。
不過在後來,由於c++天生不具備oc那種可以指定每一個參數的名稱的能力,所以,cocos2d-x的設計者決定使用c++的函數重載來解決這個問題。這也是後來爲什麼2.0版本以後,都使用create函數的重載版本了。
雖然接口簽名改掉了,但是本質並沒有變化,還是使用的二段構建。二段構建並沒有什麼不好,只是更加突出了對象需要初始化。在某種程度上也可以說是一種設計強化。因爲忘記初始化是一切莫名其妙的bug的罪魁禍首。同時,二段構建出來的對象都是autorelease的對象,而autorelease對象是使用引用計數來管理內存的。客戶端程序員在使用此接口創建對象的時候,無需關心具體實現細節,只要知道使用create方法可以創建並初始化一個自動釋放內存的對象即可。
在一點,在《Effective Java》一書中,也有提到。爲每一個類提供一個靜態工廠方法來代替構造函數,它有以下三個優點:
與構造函數不同,靜態方法有名字,而構造函數只能通過參數重載。
它每次被調用的時候,不一定都創建一個新的對象。比如boolean.valueof(boolean)。
它還可以返回原類型的子類型對象。
因此,使用二段構建的原因有如下幾點:- 兼容性、歷史遺留原因。(這也再次印證了一句話,一切系統都是遺留系統,呵呵)
二段構建有其自身獨有的優勢。
構造函數執行期間是不能調用virtual函數的(即使調用了virtual,編譯器也會用靜態調用機制而不是virtual機制,詳見Effective C++條款9),如果不用二段建構方式,在基類的構造函數裏就不能調用virtual函數實現子類需要定製化的功能,比如當需要採用模板方法這樣的設計模式做初始化的時候。但如果使用二段建構,就可以把這部分放在init()裏,實現了初始化時使用模板方法的方式。構造函數裏無法通過irtual函數實現虛函數機制,但init函數調用的時候,就可以調用virtual函數了(感謝nichos)
如果在構造函數中調用可能異常退出的函數,那麼當異常發生,函數調用棧馬上彈出,直到找到try cathc爲止。也就是說分配出的內存來不急釋放(在構造函數裏發生異常,甚至連分配出的內存指針都拿不到),函數執行就中止了。進行兩段構造可以提供一個進行try catch的機會,Symbian的兩段構造+清除棧的處理方式比這裏提到的策略安全的多。(感謝omega)
3.使用此模式的優缺點是什麼?
優點:
顯示分開內存分配和初始化階段,讓初始化地位突出。因爲程序員一般不會忘記分配內存,但卻常常忽略初始化的作用。
見上面分析《Effective Java》的第1條:“爲每一個類提供一個靜態工廠方法來代替構造函數”
除了完成對象構建,還可以管理對象內存。
缺點:
1.不如直接使用構造函數來得直白、明瞭,違反直覺,但這個是相對的。
4.此模式的定義及一般實現
定義:將一個對象的構建分爲兩個步驟來進行:1.分配內存 2.初始化它的一般實現如下:
class Test { public: //靜態工廠方法 static Test* create() { Test *pTest = new Test; if (pTest && pTest->init()) { //這裏還可以做其它操作,比如cocos2d-x裏面管理內存 return pTest; } return NULL; } // Test() { //分配成員變量的內存,但不初始化 } bool init(){ //這裏初始化對象成員 return true; } private: //這裏定義數據成員 };
5.在遊戲開發中如何運用此模式
這個也非常簡單,就是今後在使用cocos2d-x的時候,如果你繼承CCSprite實現自定義的精靈,你也需要按照“二段構建”的方式,爲你的類提供一個靜態工廠方法,同時編寫相應的初始化方法。當然,命名規範最好和cocos2d-x統一,即靜態工廠方法爲create,而初始化方法爲initXXXX。
6.此模式經常與哪些模式配合使用
由於此模式在GoF的設計模式中並未出現,所以暫時不討論與其它模式的關係。
最後看看cocos2d-x創始人王哲對於爲什麼要設計成二段構建的看法:
“其實我們設計二段構造時首先考慮其優勢而非兼容cocos2d-iphone. 初始化時會遇到圖片資源不存在等異常,而C++構造函數無返回值,只能用try-catch來處理異常,啓用try-catch會使編譯後二進制文件大不少,故需要init返回bool值。Symbian, Bada SDK,objc的alloc + init也都是二階段構造”。歡迎讀者批評指正。