C++软件开发模块化的研究(3)
<3> 依赖和版本管理
依赖是一件可怕的事情吗?
我个人认为依赖关系是软件开发过程彻底崩溃的源泉之一.有一次我企图借用另一同事开发的一个窗口控件.首先我从他那获得了.h和.cpp文件,(假设我得到的是.h和.lib,其实结果也会差不多).此时我发现.h文件中包含了另外几个.h文件.我不得不再次拷贝,而且一些.h文件必须结合.cpp组件才能编译连接.如此循环,最后带来超过100个源代码文件.我试图从中剥离我所需要的控件,但是发现非常困难,这些文件之间紧密耦合,互相依赖.取出其中任何一个有用的部件都非常困难.
实际上这样的代码基本上是无用的代码.也许在一个项目中,它已经起到了它应该起的作用.但是当我们再次需要这样的功能,或者仅仅想做一些功能上的改进的时候,我们不得不重新做起,忘记它们.因为有太多的依赖关系使那些代码绑定在一起.无法分离.
当一个中小规模的软件开发项目不断升级膨胀,而且不加任何管理的情况下,最终产生的大量代码都是如此,最后开发过程崩溃只是个时间问题,主要看产品产生之前崩溃(开发失败),还是产品产生之后(幸运成功,但是谁都不想继续折磨自己了).
1.必须严厉禁止模块之间的循环依赖.
这是很多专著中的共识.实际上在c++开发中这是完全可能的.A依赖B,B依赖C,C依赖A这种情况,导致A,B,C三个模块没有任何一个可以单独使用,实际上模块划分已经失去了意义.
2.不要让模块接口的间接依赖干扰你的开发.
开发过程中,如果始终只要关心直接依赖的模块的头文件,工作会轻松许多.当然二进制的间接依赖是不可以避免的.也就是说,如果我的工程依赖模块A(a.h,a.lib),而模块A依赖模块B,那么,我仅仅需要
#include "a.h"
则轻松搞定.当然连接的时候,我还是需要b.lib.这是二进制间接依赖.而接口方面则没有间接依赖的问题.
当然,倘若我的工程中确实使用了B模块,则情况应该是这样:
#include "a.h"
#include "b.h"
根据前两节所论述的一些原则,a.h和b.h中都不含有另外的#include指定,所以不会引入更多依赖.
下面我们用箭头表示依赖关系. A->B,则表示A依赖B.
每个模块都可能会不断升级,而不论它在依赖树的哪个节点上.假设有这样一个依赖树:
A->B->C-+->E
|
+->D->F
A本来一直运行良好.但是树中某个模块升级之后,A运行出错.此时A最需要的是恢复原来版本的模块.头文件的影响甚小(仅仅涉及模块B),主要是二进制依赖.可以在开发A的时候,对所有CVS服务器上的lib打上标记.这样失败的时候,我能把以前运行良好的lib再check out出来.
这个bug将有B来修改(如果A模块开发者提交B模块的Bug报告),但是一个依赖模块具体版本图将提交给B模块开发者作为参考:
无bug状态:
A->B(1-0-1)->C(1-2-2)-+->E(1-2-4)
|
+->D(2-1-1)->F(3-2-4)
出错状态:
A->B(1-0-1)->C(1-2-4)-+->E(1-2-4)
|
+->D(2-1-2)->F(3-2-4)
对B模块的开发者来说,是非常好的参考,显然问题在于C和D的版本升级.如果B开发者能提出模块C的bug报告,那么问题将由C开发者解决.反之,B必须自己解决.
以上的管理方式如果要良好运作,至少应该有以下几个条件:
1.有良好的方式避免模块开发者之间互相推诿责任,拖延bug的解决,并必须使他们需要的沟通减少到最少.否则招来的是没完没了的会议和讨论,沟通的时间挤占开发的时间.
2.能方便的取得和管理依赖树上每个模块的版本.这是一个纯技术的问题.
合理的分工可以极大的减少需要的沟通.首先我们确定同一个模块只由一人(或者管理上视为一人的一组人)开发维护,但是绝不是一人只能开发一个模块.相反,我主张模块不要太大(巨大的模块带来的问题总是比小模块多),因此一人开发多个模块是正常的和必然的.考虑上边的树:
A->B->C-+->E
|
+->D->F
A与B由一人开发是合理的.此人只需要直接依赖模块C.C和E由一人开发也是合理的,此人仅仅需要直接依赖D.同理D和F可以由一人开发,此人仅仅需要向C的开发者负责.
如果是两人划分,A与B一人开发,C,E,D,F一人开发不错.或者ABCE/DF的划分也不错.
反之,AC/BE这样的划分,就非常让人难堪了.此二人有相互依赖的关系,导致更多的沟通.
1.让开发者和开发者之间的关系尽量保持单线单向负责.
2.把让开发者之间的负责关系减少到最少.
3.关键模块和与之依赖的无关紧要的旁支交给同一个人开发.
如下图:
A->B->C-+->E-+
| |
+->D-+->F
所谓的关键模块是和与之相关的模块多的模块,无关紧要的旁支则是与之有关系的模块少的模块.现在统计一下各个模块的"关键性":
A : 1
B : 2
C : 3
E : 2
D : 2
F : 1
以上数字实际上是一个节点的相邻节点数而已.
F和A可以说是旁支,仅仅和一个模块有关系.而C是关键模块.C应该和A,B交给同一个人开发,或者至少应该附加B或者E.
有专门的软件可以解决"方便的取得和管理依赖树上每个模块的版本"的问题.