1)文件间存在的编译依存关系
在C++中,如果没有将接口和实现分离,那么在修改起一个成分,就有可能导致编译时许多文件的重新编译,如定义类如下:
1 class Person 2 { 3 public: 4 Person(const string& name, const Date& birthday, const Address& addr); 5 string name() const; 6 string birthDate() const; 7 string address() const; 8 private: 9 string theName();10 Date theBirthDate;11 Address theAddress;12 };
由于Person类中使用到了string,Date和Address的定义式,因此需要#include一些头文件,即在文件的开始处需要下面的指令:
1 #include2 #include "Date.h"3 #include "Birthday.h"
这样一来,如果这些头文件或者这些头文件所依赖的其他头文件有任何的改变,那么任何包含Person或任何使用Person的文件都需要重新编译,从而形成连串编译依存性,给项目带来灾难。
2)降低文件间的编译依存关系
针对上面的问题 ,也许有程序员会这样设想:将实现细目于类的定义分开,即:
1 namespace std 2 { 3 class string; 4 } 5 class Date; 6 class Address; 7 class Person 8 { 9 public:10 Person(const string& name, const Date& birthday,const Address& addr);11 string name() const;12 string birthDate() const;13 string address() const;14 };
这样做的话,只需要在Person的接口发生改变时才需要重新编译。但上面的代码不会通过编译,原因有两个:第一string不是个class,而是typedef ,真正的前置声明比较复杂;第二 编译器在编译期间必须知道对象的大小从而分配足够的空间来存放对象,按照上面的定义方法,编译器不知道该为Person分配多少的内存空间。
如果采用类似Java和Smalltalk的方式:编译器在编译期间不需要知道类的大小,只需要分配一个指针大小的空间来指向对象,也即在C++中将类中的实现细节隐藏在一个指针的背后,那么就可以将接口和实现分离,降低编译依存性。具体的讲,就是将Person类分割为两个类,一个只提供接口,一个用来实现接口,如果定义这个实现接口的类为PersonImpl,那么Person的定义如下:
1 #include2 #include 3 4 class PersonImpl; 5 class Date; 6 class Address; 7 8 class Person 9 {10 public:11 Person(const string& name, const Date& birthday, const Address addr);12 string name() const;13 string birthDate() const;14 string address() const;15 ...16 private:17 tr1::shared_ptr pImpl;18 };
在这种设计之下,Person的客户就完全与Dates, Address以及Person的实现细目分离了,这些classes的任何修改都不需要Person客户端重新编译。此外,由于客户无法看到Person的实现细目,也就不能写出“取决于这些细目”的代码,从而真正实现“接口与实现分离”。
上面分离实现的关键是将“声明的依存性”替换“定义的依存性”,这正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,如果做不到,则让它与其他文件内的声明式而非定义式相依。具体的讲:
a. 如果使用object references 或object pointers可以完成任务,就不要使用objects.你可以只靠一个类型声明式就定义出指向该类型的references和pointers;但如果定义某类型的objects,就需要用到该类型的定义式。
b. 如果能够,尽量以class声明式替换class定义式。通常当你声明一个函数而它用到某个class时,你并不需要该class的定义,如:
1 class Date;2 Date today();3 void clearAppointments(Date d);
实际调用这两个函数时,Date的定义需要先出现,这样看来似乎用class声明代替class定义是多余的,但事实并非如此。因为对于这个函数,并非每个用户都会调用,对于那些不调用这样的函数的用户,用class声明替换class定义很现任可以降低编译依存性。
c. 为声明式和定义式提供不同的头文件,这样一来就引入了handle class 和 interface class.
以上整理自Effective C++中文版第三版case 31.
未完待续。