这个原则着重讲述的是编译的依存关系,篇幅很长,理解起来也比较费劲。
这个原则是针对什么问题而提出来的呢?有的时候你在一个大工程里,你就修改了某个类中的一小部分实现,甚至就是一条语句,结果整个工程各种莫名其妙的错误出现了,然后你崩溃了。
作者说这是由于没有把接口从实现中分离造成的,那这又是什么意思呢?从作者所举的例子Person类来看,它的私有成员里面有很多其他类的对象,而这些对象又是Person类中某些函数的参数,如下图所示:
从这图可以看出作者所说的实现也就是私有成员中所列的这些东西,而它们又是其他类的对象。那既然用到了其他类那必须要引用其他类的头文件啊,就是使用#include命令。这样的话就形成了一定的编译依赖关系,这名词还是很有学术气息的。那形成编译依赖关系又能怎么样?那可以用一句话来概括——牵一发而动全身。
作者接着引用了一种惯常的思维,既然你说不要把实现和接口掺和在一起,那你就不实现呗,让那些被引用的类也只不过是类的声明而已不就得了,让后把它们一块放在命名空间里面。这种类的声明叫做前置声明。这样做的好处就是实现不会动,会动的只能是接口,那么用户只需要在接口被改动之后重新编译即可。
不过上面这种办法纯属扯淡!因为编译器必须知道编译期间某个对象的大小,编译器只能通过类的定义才能知道这个对象需要多大空间,现在你就给了一个声明,编译器哪知道那个对象到底需要多大地方?!在这一点上C++和Smalltalk,Java还是有区别的,因为后两者只提供指向类的指针,而指针大小是固定的。
于是乎得出了一种常用的设计方式,那就是接口和实现分离,再具体点就是一个类提供接口,另一个类提供实现。而接口类和实现类中连接的纽带就是一个作为指向实现类的私有智能指针。这种设计一般被称为pimpl(pointer to implementation)。
它体现的思想是使用生命的依存性去替换实现的依存性,这是编译依存性最小化的本质体现。
很奇怪,你仅仅是声明一个类,你就能用这个类定义一个形参并且放在函数形参列表中。其实还是那种情况,你只需要在用到定义的时候才真正去暴露类的定义,函数也是同理,所以你可以看到某个头文件中有很多声明式包括函数的和类的。那么你也会看到我经常把类和函数的声明的放到头文件中去,而把实现放到CPP中去。这样做的目的是降低与实现文件之间的编译依存关系。
作者还提到有些泛型类也是采用实现和声明分离的设计方式的,不过要使用关键字export,可是这一关键字已经很少出现在当代编译器当中了,所以我所见到的泛型都是实现和生命合而为一的。
从下面这个代码段可以看出在构造函数的形参列表中类的对象是可以new的,如下图所示:
另一种pimpl实现手段是写一个C++的interface,里面是virtual函数和pure virtual虚拟函数,当然interface里面可以含有成员变量,但是人们通常不那么做,因为就好像它的名字那样interface只是接口,而接口是供别人调用用的,所以它里面只需要函数足矣。Interface肯定是要被继承的,否则它啥用都没有,尤其是内涵pure virtual的interface。这个实现的任务就交给了它的子类,在这个interface中有那么一个函数,它的作用是返回一个已经实例化的对象的指针。这个函数被称为factory函数或者virtual构造函数,当然后者并不是说构造函数可以virtual。这些做法都是基于内联实现的。
上述这些做法多多少少会带来效率的低下。
总结一下作者的思想就是:
1、依赖于声明式,使用pimpl模式进行设计都会减小编译依存性。
2、头文件应该只包含声明。