所谓二段构建,就是指创建对象时不是直接通过构建函数来分配内存并完成初 始化操作。取而代之的是,构造函数只负责分配内存,而初始化的工作则由一 组合起来,完成最终对象的构建。因为在《Cocoa设计模式》一书中,把此 惯用法称之为“Two Stage Creation”,即“二段构建”。
Cocos2D-X 3.0以后的二段构造更纯粹了,它把所有类的构造函数、析构函数以及init方法全部变成protected方法。强制开发者只能通过create工厂方法来创建对象。这样做的好处有两个:
一、可以更好地配合Cocos2D-X内部提供的基于引用计数的内存管理模型,因为create方法按照约定会返回一个autorelease对象。
二、create工厂方法可以有参数,例如createWithFileName,createWithSpriteFrameName,这样比构造函数更具有可读性。因为构造函数的重载可读性真的很差。这一点,我特别喜欢Objective-C的中缀表达式语法,虽然要多写一些字符,但是可读性真的很棒。
1.应用场景:
二段构建在cocos2d-x里面随处可见,自从3.0版本以后,所有的二段构建方法的签名都改成create了。这样做的好处是一方面统一接口,方便记忆,另一方面是以前的类似Cocoa的命名规范不适用c++,容易引起歧义。下面以Sprite为类,来具体阐述二段构建的过程,请看下列代码:
Sprite* Sprite::create()
{
Sprite *sprite = new (std::nothrow) Sprite();
if (sprite && sprite->init())
{
sprite->autorelease();
return sprite;
}
CC_SAFE_DELETE(sprite);
return nullptr;
}
如上面代码中的注释所示,创建一个sprite明显被分为两个步骤:1.使用new来创建内存;2.使用initXXX方法来完成初始化。
因为Sprite的构造函数也有初始化的功能,所以,我们再来看看Sprite的构建函数实现:
Sprite::Sprite(void)
: _shouldBeHidden(false),_texture(nullptr),_insideBounds(true)
{
}
很明显,这个构建函数所做的初始化工作非常有限,仅仅是在初始化列表里面初始化了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()里,实现了初始化时使用模板方法的方式。构造函数里无法通过virtual函数实现虚函数机制,但init函数调用的时候,就可以调用virtual函数了(感谢nichos)
如果在构造函数中调用可能异常退出的函数,那么当异常发生,函数调用栈马上弹出,直到找到try cathch为止。也就是说分配出的内存来不急释放(在构造函数里发生异常,甚至连分配出的内存指针都拿不到),函数执行就中止了。进行两段构造可以提供一个进行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的时候,如果你继承Sprite实现自定义的精灵,你也需要按照“二段构建”的方式,为你的类提供一个静态工厂方法,同时编写相应的初始化方法。当然,命名规范最好和cocos2d-x统一,即静态工厂方法为create,而初始化方法为initXXXX。
6.此模式经常与哪些模式配合使用
由于此模式在GoF的设计模式中并未出现,所以暂时不讨论与其它模式的关系。
最后看看cocos2d-x创始人王哲对于为什么要设计成二段构建的看法:
“其实我们设计二段构造时首先考虑其优势而非兼容cocos2d-iphone. 初始化时会遇到图片资源不存在等异常,而C++构造函数无返回值,只能用try-catch来处理异常,启用try-catch会使编译后二进制文件大不少,故需要init返回bool值。Symbian,Bada SDK,objc的alloc + init也都是二阶段构造”。