假如一个系统中有多个模块,不妨命名为 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 模式不应被用来处理系统的主体模块, 它真正的适用场合之一是实现对插件的支持, 把所有插件定义在一个列表中, 然后逐项处理, 因为这时系统的主体功能已经完成, 所以可以为插件定义出清晰的接口, 而且就算定义的接口有一点问题, 它所影响的也只是某些插件而非主体功能了。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于