乍一看标题,大家可能会觉得很奇怪,神马是“二段构建模式”呢?

所谓二段构建,就是指创建对象时不是直接通过构建函数来分配内存并完成初始化操作。取而代之的是,构造函数只负责分配内存,而初始化的工作则由一些名为initXXX的成员方法来完成。然后再定义一些静态类方法把这两个阶段组合起来,完成最终对象的构建。因为在《Cocoa设计模式》一书中,把此惯用法称之为“Two Stage Creation”,即“二段构建”。

2014.6.1号更新:

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为类,来具体阐述二段构建的过程,请看下列代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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的构建函数实现:

1
2
3
4
5
6
    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()里,实现了初始化时使用模板方法的方式。构造函数里无法通过irtual函数实现虚函数机制,但init函数调用的时候,就可以调用virtual函数了(感谢nichos)

  • 如果在构造函数中调用可能异常退出的函数,那么当异常发生,函数调用栈马上弹出,直到找到try cathc为止。也就是说分配出的内存来不急释放(在构造函数里发生异常,甚至连分配出的内存指针都拿不到),函数执行就中止了。进行两段构造可以提供一个进行try catch的机会,Symbian的两段构造+清除栈的处理方式比这里提到的策略安全的多。(感谢omega)

3.使用此模式的优缺点是什么?

优点:

  • 显示分开内存分配和初始化阶段,让初始化地位突出。因为程序员一般不会忘记分配内存,但却常常忽略初始化的作用。

  • 见上面分析《Effective Java》的第1条:“为每一个类提供一个静态工厂方法来代替构造函数”

  • 除了完成对象构建,还可以管理对象内存。

缺点:

  • 1.不如直接使用构造函数来得直白、明了,违反直觉,但这个是相对的。

4.此模式的定义及一般实现

定义:将一个对象的构建分为两个步骤来进行:1.分配内存 2.初始化它的一般实现如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    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也都是二阶段构造”。

欢迎读者批评指正,