读书笔记_Effective_C++_条款三十一:将文件间的编译依存关系降至最低(第二部分)
下面再来看书,去理解书上说的Handler classes就简单多了,我们大概过一下。
假设我们要写一个Person类,如下:
1 class Person 2 { 3 private: 4 string name; 5 MyDate birthday; 6 MyAddress address; 7 8 public: 9 // fallows functions 10 // ... 11 };
这个Person类里面包含有人的名字,人的生日以及地址,还有一些没有列出来的方法。注意到这里用到了string(它不是一个class,只是一个typedef,大多数情况下,我们认为它不会有任何改变),以及自定义的类MyDate与MyAddress,一种常见的做法是采用include头文件,像这样:
1 #include <string> 2 #include "MyDate.h" 3 #include "MyAddress.h"
在MyDate.h里面写好日期类相关的成员变量与方法,而在MyAddress.h里面写好地址类相关的成员变量与方法。但如果此后要往MyDate类或者MyAddresss类添加成员变量,那么不仅仅所有用到MyDate或者MyAddress对象的文件需要重新编译,而且所有用到Person对象的文件也需要重编译,一个小改动竟然会牵涉到这么多的地方!
可以把问题归结为“C++并没有把将接口从实现中分离这件事做好”,因为包含头文件的做法很直观很方便,用的人也很普遍,而C++并没有对这种做法加以限制。
如果要把编译的依赖性降低,就要换一种思路来处理,不能出现定义式,只能出现声明式,代价是增加代码的复杂度以及性能上的一些损失。
书上提到了两种方法,第一种是采用Handler Classes(用指针指向真正实现的方法),第二种是Interface Classes(抽象基类)。
对于第一种Handler Class,一句话,就是.h里面不包含类的自定义头文件,用“class 类名”的声明方式进行代替(也要把相应的成员变量替换成指针或引用的形式),在.cpp文件里面包含类的自定义头文件去实现具体的方法。改造之后的程序看起来是这样子的:
1 // Person.h 2 #include <string> 3 using namespace std; 4 5 class PersonImp; 6 7 class Person 8 { 9 private: 10 //string Name; 11 //MyDate Birthday; 12 //MyAddress Address; 13 PersonImp* MemberImp; 14 15 public: 16 string GetName() const; 17 string GetBirthday() const; 18 string GetAddress() const; 19 // follows functions 20 // ... 21 };
1 // Person.cpp 2 #include "PersonImp.h" 3 #include "Person.h" 4 5 string Person::GetName() const 6 { 7 return MemberImp->GetName(); 8 } 9 string Person::GetBirthday() const 10 { 11 return MemberImp->GetName(); 12 } 13 string Person::GetAddress() const 14 { 15 return MemberImp->GetAddress(); 16 }
1 // PersonImp.h 2 #ifndef PersonImp_H 3 #define PersonImp_H 4 5 #include <string> 6 #include "MyAddress.h" 7 #include "MyDate.h" 8 using namespace std; 9 10 class PersonImp 11 { 12 private: 13 string Name; 14 MyAddress Address; 15 MyDate Birthday; 16 17 public: 18 string GetName() const 19 { 20 return Name; 21 } 22 23 string GetAddress() const 24 { 25 return Address.ToString(); 26 } 27 28 string GetBirthday() const 29 { 30 return Birthday.ToString(); 31 } 32 }; 33 34 #endif /* PersonImp_H */
1 // MyDate.h 2 #ifndef MY_DATE_H 3 #define MY_DATE_H 4 5 #include <string> 6 using namespace std; 7 8 class MyDate 9 { 10 private: 11 int Year; 12 int Month; 13 int DayOfMonth; 14 15 public: 16 string ToString() const; 17 } 18 #endif /* MY_DATE_H */
1 // MyAddress.h 2 #ifndef MY_ADDRESS_H 3 #define MY_ADDRESS_H 4 5 #include <string> 6 using namespace std; 7 8 class MyAddress 9 { 10 private: 11 string Country; 12 string Province; 13 string City; 14 string Street; 15 16 public: 17 string ToString() const; 18 }; 19 20 #endif /* MY_ADDRESS_H */
这里有一点要说一下,在Person.h里面并没有使用MyDate*和MyAddress*,而是用了PersonImp*,由PersonImp里面包含MyDate与MyAddress,这样做的好处就是方便统一化管理,它要求PersonImp里面的方法与Person的方法是一致的。以后Person添加成员变量,可以直接在PersonImp中进行添加了,从而起到了隔离和隐藏的作用,因为客户端代码大量使用的将是Person,而不必关心PersonImp,用于幕后实现的PersonImp只面向于软件开发者而不是使用者。
书上是用shared_ptr来管理PersonImp的,使资源管理上更加科学与合理。
另外,书上也提倡把class x; class xx; class xxx;的声明放至一个名为”xxxfwd.h”的头文件里,比如”datefwd.h”,这个头文件里面只有声明式,而没有具体的类细节。也就是说,对于某个类,比如MyDate,应该分出三个文件,一个是datefwd.h,里面是一些用到的外来的class声明式;一个是MyDate.h里面是MyDate类的结构声明;一个是MyDate.cpp,它与MyDate.h配对,给出具体的实现细节。