假如一个系统中有多个模块,不妨命名为Module1, Module2, Module3…, 毫无疑问这个系统的启动过程中需要初始化所有这些模块, 而退出时要销毁它们, 那应该用下面哪种方法来完成这个任务呢?

A. 让这些模块都支持一个IModule, 然后定义一个IModule*类型的数组, 把这些模块的指针都加进去:

IModule* modules[] = {&Module1, &Module2, &Module3, ...};

// 初始化时:
for(int i = 0; i < sizeof(modules)/sizeof(modules[0]); ++i)
    modules[i]->Init();
 
// 退出时:
for(int i = sizeof(modules)/sizeof(modules[0]) - 1; i >= 0; --i)
    modules[i]->Uninit();

B. 老老实实的一个一个的来:

// 初始化时:
Module1.Init();
Module2.Init();
Module3.Init();
...
// 退出时:
...
Module3.Uninit();
Module2.Uninit();
Module1.Uninit();

如果你读了我的上一篇, 你肯定能猜到我的选择是B. 但我想先说说A, 把A说清楚了, 选择B的理由也就出来了。

A是典型的数据驱动 + Builder模式, 它最大的优点是增加或删除一个模块只需要增加或删除一个数据项, 耦合很小, 所以看起来非常优雅。

而A的缺点有两个. 和上一篇一样, 其中之一也出在调试上: 当一个模块初始化失败后, 如果我们只看外面这些代码, 没有办法一眼得出是谁失败了, 必须得多一些操作才行。

第二点是A实现强制了模块的初始化和退出顺序, 先初始化的模块后退出貌似很合理, 但在一个大型系统中却总会出例外, 而且还可能出现Module1先初始化一半, 然后Module2初始化, 之后Module1再继续初始化等情况. 当然, 我们可以使用"把初始化顺序和退出顺序定义在两个数组中"或"把初始化划分为多个阶段"等方法处理这些问题, 但这些方法都会增加复杂性, 而且也都不能从根本上解决问题。

B实现则用简单直接的方法很好的避免了A的问题, 虽然它看起来好像很笨, 增加删除一个模块要改多个地方, 但这些改动总共也不过几行代码, 而且往往只涉及一个文件, 所以总体代价并不高。

最后, 本文的场景乍看起来非常适合使用Builder模式, 可为什么使用它的效果不好呢? 我本人对设计模式不感冒也不擅长, 所以只能试着解释一下这个问题:其原因就是这个场景只是看起来像, 但其实并不适用Builder模式. Builder模式要求对象支持统一的接口, 也希望对象之间没什么关联, 这是我们作设计时追求的目标, 但在实现一个复杂系统时却很难完全满足这些要求, 所以硬套上去就会出问题。而且在实现一个系统时, 各个模块还不可能完全定下来, 实现过程中的改动也会给Buidler模式带来麻烦。按我个人的理解, Buidler模式不应被用来处理系统的主体模块, 它真正的适用场合之一是实现对插件的支持, 把所有插件定义在一个列表中, 然后逐项处理, 因为这时系统的主体功能已经完成, 所以可以为插件定义出清晰的接口, 而且就算定义的接口有一点问题, 它所影响的也只是某些插件而非主体功能了。