下面来谈谈书中的第二部分,用Interface Classes来降低编译的依赖。从上面也可以看出,避免重编的诀窍就是保持头文件(接口)不变化,而保持接口不变化的诀窍就是不在里面声明编译器需要知道大小的变量,Handler Classes的处理就是把变量换成变量的地址(指针),头文件只有class xxx的声明,而在cpp里面才包含xxx的头文件。Interface Classes则是利用继承关系和多态的特性,在父类里面只包含成员方法(成员函数),而没有成员变量,像这样:
1 // Person.h 2 #include <string> 3 using namespace std; 4 5 class MyAddress; 6 class MyDate; 7 class RealPerson; 8 9 class Person 10 { 11 public: 12 virtual string GetName() const = 0; 13 virtual string GetBirthday() const = 0; 14 virtual string GetAddress() const = 0; 15 virtual ~Person(){} 16 };
而这些方法的实现放在其子类中,像这样:
1 // RealPerson.h 2 #include "Person.h" 3 #include "MyAddress.h" 4 #include "MyDate.h" 5 6 class RealPerson: public Person 7 { 8 private: 9 string Name; 10 MyAddress Address; 11 MyDate Birthday; 12 public: 13 RealPerson(string name, const MyAddress& addr, const MyDate& date):Name(name), Address(addr), Birthday(date){} 14 virtual string GetName() const; 15 virtual string GetAddress() const; 16 virtual string GetBirthday() const; 17 };
在RealPerson.cpp里面去实现GetName()等方法。从这里我们可以看到,只有子类里面才有成员变量,也就是说,如果Address的头文件变化了,那么子类一定会重编,所有用到子类头文件的文件也要重编,所以为了防止重编,应该尽量少用子类的对象。利用多态特性,我们可以使用父类的指针,像这样Person* p = new RealPerson(xxx),然后p->GetName()实际上是调用了子类的GetName()方法。
但这样还有一个问题,就是new RealPerson()这句话一写,就需要RealPerson的构造函数,那么RealPerson的头文件就要暴露了,这样可不行。还是只能用Person的方法,所以我们在Person.h里面加上这个方法:
1 // Person.h 2 static Person* CreatePerson(string name, const MyAddress& addr, const MyDate& date);
注意这个方法是静态的(没有虚特性),它被父类和所有子类共有,可以在子类中去实现它:
1 // RealPerson.cpp 2 #include “Person.h” 3 Person* Person::CreatePerson(string name, const MyAddress& addr, const MyDate& date) 4 { 5 return new RealPerson(name, addr, date); 6 }
这样在客户端代码里面,可以这样写:
1 // Main.h 2 class MyAddress; 3 class MyDate; 4 void ProcessPerson(const string& name, const MyAddress& addr, const MyDate& date);
// Main.cpp #include "Person.h" #include “MyAddress.h”; #include “MyDate.h”; void ProcessPerson(const string& name, const MyAddress& addr, const MyDate& date) { Person* p = Person::CreatePerson(name, addr, date); … }
就可以减少编译依赖了。
总结一下,Handler classes与Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。减少编译依存性的关键在于保持.h文件不变化,具体地说,是保持被大量使用的类的.h文件不变化,这里谈到了两个方法:Handler classes与Interface classes。
Handler classes化类的成员变量为指针,在.h文件里面只包含class xxx的外来类声明,而不包含其头文件,在.cpp涉及到具体外来类的使用时,才包含xxx.h的头文件,这样最多只影响本身类的cpp重编,但因为.h文件没有变化,所以此类的对象存在的文件不必重编。
当然,书上说的Handler classes更想让我们在类A的基础上另造一个中间类AImp(成员函数完全与类A一致),这个中间类的成员中里面放置了所有类A需要的外来类的对象,然后类的逻辑细节完全在Almp.cpp中实现,而在A.cpp里面只是去调用Almp.cpp的同名方法。A.h的成员变量只有Almp的指针,这看上去好像一个Handler,因此而得名。
Interface classes则是将细节放在子类中,父类只是包含虚方法和一个静态的Create函数声明,子类将虚方法实现,并实现Create接口。利用多态特性,在客户端只需要使用到Person的引用或者指针,就可以访问到子类的方法。由于父类的头文件里面不包含任何成员变量,所以不会导致重编(其实由于父类是虚基类,不能构造其对象,所以也不用担心由于父类头文件变化导致的重编问题)。
请记住:
1. 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式,基于此构想的两个手段是Handler classes和Interface classes。
2. 程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及templates都适用。