在前面一篇文章《真刀真枪模块化(1)——一本糊涂账》中,我们讨论了:
以及
道理说起来简单,真要实际操作起来,一线开发人员往往会直摇头:手中已有的所谓“模块”质量参差不齐、模块的开发者鱼龙混杂、很多模块别说出了问题要找开发方负责维护了,就是原作者是谁恐怕都找不到了——在这种情况下,大谈“禁止开发人员阅读模块的实现代码”,简直就是天方夜谭,颇有几分“何不食肉糜”的傲慢。
——难道模块化本身错了么?实际情况并非如此,这里傻孩子忍不住想“感慨”两句:在追求和实践新的方法(论)的时候,总难免会遇到这样那样的困难,有的困难甚至让整个方案看起来“完全行不通”——在这种时候,如果立即退出来将整个方法全盘否定,就会失去宝贵的前进机会。
在模块化的过程中,要想发挥模块化“复用已有代码”、“降低开发时间”的作用,就必须将模块视作黑盒子;一旦模块被视作黑盒子,实现的质量和后续的可靠维护就成为当前模块是否可用的基石——进一步来说,不靠谱的代码实现和差强人意的接口设计与封装是导致模块化失败的根本原因。
本文将为您介绍一种模块化封装的简单操作方式——由傻孩子根据十多年工程实践经验总结、历经无数商业项目的千锤百炼。通过这一方式构建的模块,我称之为服务(Service),因此,这里所要介绍的模型又被称之为“Service模型”。
【正文】
从具体操作层面来说,所谓Service模型并不复杂。
首先,每一个模块都有一个属于自己的专门的文件夹,文件夹的名称与模块名相同:

其次,每一个模块中都有一个专门的头文件,用于提供给模块的使用者来包含(#include);该头文件的名称必须与模块的名称相同。

需要特别强调和说明的是:
与接口头文件相对,每一个模块内部都会有一个专门的头文件用于实现对模块的配置:
在构建和使用模块的时候,无论是模块的设计者还是模块的使用者,都应该遵循黑盒子原则,在操作上表现为——模块的使用者不应该修改任何位于模块文件夹内部的内容——模块文件夹既是黑盒子的容器,也是黑盒子的边界。
为了遵守这一原则,模块内部的配置头文件实际上是不允许用户去修改的——那么这又如何让用户更改对模块的各个配置选项呢?答案很简单,如下图所示:模块内部的app_cfg.h 在文件的一开始会首先包含上一级目录的app_cfg.h。

为了实现这一点,一个模块内部 app_cfg.h 的固定内容格式为:
//! 作为模块的用户,不要修改这里的任何内容
#include "../app_cfg.h"
/* app_cfg.h 的防重复包含的保护宏 */
/* 请将 XXXXXX 替换为模块的名称,并删除本注释 */
#ifndef __XXXXXX_APP_CFG_H__
#define __XXXXXX_APP_CFG_H__
...
#endif /* app_cfg.h 文件的结尾 */一个模块的接口头文件,其内部格式可能为:
//! 作为模块的用户,不要修改这里的任何内容
/* 模块接口头文件防重复包含的保护宏 */
/* 请将 XXXXXX 替换为模块的名称,并删除本注释 */
#ifndef __XXXXXX_H__
#define __XXXXXX_H__
/* 模块的接口头文件在一开始要包含当前模块的 app_cfg.h,
* 这里的 "./" 不可以省略
*/
#include "./app_cfg.h"
/* 其它include */
...
#endif /* 接口头文件的结尾 */可以很容易注意到,当使用某一模块时,用户可以很方便的在模块外部定义一个属于自己的 app_cfg.h 来向模块提供配置信息——而无论如何修改这一文件,都不会破坏黑盒子本身的内容。
再次,一个模块往往拥有一个或多个C源文件,它只需要包含模块的接口头文件,就可以共享一些“对外公开的信息”。

这里有个朋友会问了:根据最小信息公开原则,接口头文件中只包含了一些最小信息,如果模块内的多个C源文件之间需要共享一些非公开的私有信息,该怎么处理呢?
为了解决这一问题,我们一般会引入一个以双下划线为前缀的接口头文件(比如,叫做__common.h),并视其为模块的私有财产。如下图所示,这一头文件是仅供模块内的源代码包含的——无论是模块的接口头文件还是模块的配置头文件都不应该对其进行包含——以防信息泄露:

一个典型的 __common.h 内容如下:
/*! 作为模块的用户,不要修改这里的任何内容,理论上也不应该关心这
* 里出现的任何内容。
* 对模块的作者来说,如果模块以 lib 的形式提供,请务必将本文件删除
*/
#ifndef __XXXXXX_COMMON_H__
#define __XXXXXX_COMMON_H__
...
#endif /* 私有接口头文件的结尾 */基于这一规则,模块内一个可能的C源文件内容如下:
//! 作为模块的用户,不要修改这里的任何内容
/* 首先包含模块的接口头文件,模块的配置头文件也会间接的被引入进来 */
#include "./xxxxx.h"
#include "./__common.h"
/* 当前C源文件私有且不想跟模块内其它C文件共享的内容: 宏、类型定义等等 */
...
/* 函数实现等等 */
...最后,一个模块内是允许包含其它子模块的,对于这种嵌套情况,仅需要两步骤就可以完成部署:

以上就是使用Service模型进行模块化的基本规则。是不是很简单?
【后记】
Service模型本身是完全本着简化用户操作的宗旨,以实用性为重中之重,同时也避免一切“反直觉”的设定。
对用户来说,这一模型是非常友好的:
对模块的开发者来说:
当然,这一Service模型也有一个小缺点(可能有些人也对此无法容忍),即,用某些工程管理工具将头文件的包含关系展开时,通常会看到海量的app_cfg.h(尽管他们内部都使用了模块特有的保护宏进行区别)——对于这一问题,在真刀真枪模块化的后续内容中,将提供一个较为完美的解决方案,这里就先卖个关子——对普通用户来说,现有的Service模型足够了。