摘要
不要过分依赖:如果用前向声明(forward declaration)能够实现,那么就不要包含(#include)定义。
不要互相依赖:循环依赖是指两个模块直接或者间接地互相依赖。所谓模块就是一个紧凑的发布单元(见“名字空间与模块”部分的引言部分)。互相依赖的多个模块并不是真正的独立模块,而是紧紧胶着在一起的一个更大的模块,一个更大的发布单元。因此,循环依赖有碍于模块性,是大型项目的祸根。请避免循环依赖。
讨论
除非确实需要类型定义,否则应该优先使用前向声明。主要在两种情形下需要某个类比如C的完整定义:
l 需要知道C对象的大小时:例如,在栈中分配一个C,或者作为另一个类型直接具有的成员分配一个C。
l 需要命名或者调用C的成员时:例如,调用成员函数时。
为了遵循本书的一贯宗旨,我们从一开始就将可能导致编译时错误的循环依赖搁置不谈,因为如果遵循了文献和本书第1条中提出的合理建议,就应该已经避免了这种情况。所以我们将注意力集中在可编译代码中的循环依赖上,探讨它们是如何影响代码质量的,以及采取什么步骤可以避免。
一般而言,应该在模块层次上考虑依赖性及其循环。模块是一同发布的类和函数的紧凑集合(见第5条和“名字空间与模块”部分的引言部分)。最简单形式的循环依赖是两个直接互相依赖的类:
class Child; // 打破循环依赖
class Parent {// ……
Child* myChild_;
};
class Child {// …… // 可能位于不同的头文件中
Parent* myParent_;
};
这里Parent和Child存在互相依赖。代码能够编译,但是有一个根本性的问题:两个类不再是独立的,而是互相依赖的了。这种情况未必很糟,但是应该只出现在两个类同属一个模块(由同一个人或者小组开发,作为一个整体进行测试和发布)的时候。
为了对比,我们考虑如下情况:如果Child不需要保存向回指向其Parent对象的链接又会怎么样呢?那么Child就可以独自作为一个更小的模块(可能名字不同)发布,完全独立于 Parent——这种设计显然更加灵活。
如果依赖循环跨越多个模块(这些模块将因为依赖关系而联合起来形成一个大的发布单元)的话,情况只会变得更糟。这正是为什么称循环是模块性最凶恶的敌人的原因。
为了打破循环,可以应用[Martin96a]和[Martin00](另见第36条)中记载的依赖倒置原理(Dependency Inversion Principle):不要让高层模块依赖于低层模块;相反,应该让两者都依赖于抽象。如果能够为Parent或Child定义独立的抽象类,那么就能够打破循环了。否则,就必须保证它们属于同一模块。
依赖有一种特殊形式,一些设计颇受其害:派生类的传递依赖(transitive dependency),即基类依赖于所有的派生类,包括直接的和间接的。Visitor(访问器)设计模式的一些实现就会导致这种依赖。它只对极为稳定的类层次而言是可以接受的。否则可能需要修改设计,例如使用Acyclic Visitor(非循环访问器)模式[Martin98]。
过度相互依赖的一个症状,就是局部发生变化时需要进行增量构建,不得不重新编译项目中的很大一部分代码(见第2条)。
例外情况
类之间的依赖循环并不一定都是坏事——只要类被认为属于同一模块,一起测试,一起发布。诸如Command和Visitor等设计模式的原始实现就会产生天生相互依赖的接口。这种相互依赖可以被打破,但是需要进行明确的设计才行。





