1. 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
2. 程序库头文件应该以”完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及 templates 都适用。
问题描述
假设你对 C++ 程序的某个 class 实现文件做了些轻微修改。注意,修改的不是 class 接口,而是实现,而且只改 private 成分。然后重新构建这个程序,但你会发现这可能需要很长时间进行编译链接。
问题出在 C++ 并没有把”将接口从实现中分离“这事做得很好。Class 的定义式不只详细叙述了 class 接口,还包括许多的实现细节,例如:
#include <string>
#include "date.h"
#include "address.h"
class Person {
public:
Person(const std::string& name, const Date& birthday,
const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::string theName; // 实现细节
Date theBirthDate; // 实现细节
Address theAddress; // 实现细节
};
由于在 Person 定义文件中含入了其它头文件,那么 Person 文件和这些含入的文件之间形成了一种编译依存关系。如果这些头文件中有任何一个被改变,或这些头文件所倚赖的其他头文件有任何改变,那么每一个含入 Person class 的文件就得重新编译,任何使用 Person class 的文件也必须重新编译。这样的连串编译依存关系会对许多项目造成难以形容的灾难。
解决方式一:Handle classes
因此,针对 Person 我们可以这样做:把 Person 分割成两个类,一个只提供接口,另一个负责实现该接口。如果负责设计的那个所谓的实现类取名为 PersonImpl,那么 Person 将定义如下:
#include <string>
#include <memory> // 为了使用tr1::shared_ptr而含入
class PersonImpl; // Person实现类的前置声明
class Date; // Person接口用到的classes的前置声明
class Address;
class Person {
public:
Person(const std::string& name, const Date& birthday,
const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string addres() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl; // 指针,指向实现物
};
在这里, Person 只内含一个指针成员(这里使用tr1::shared_ptr,见条款13),指向其实现类(PersonImpl)。这样的设计常被称为”pointer to implementation“。这种 classes 内的指针名称往往就是 pImpl,就像上面代码那样。
在这样的设计下,Person 的客户就完全与 Dates、Addresses 以及 Persons 的实现细节分离了。那些 classes 的任何实现修改都不需要 Person 客户端重新编译。此外,由于客户无法看到 Person 的实现细目,也就不可能写出什么”取决于那些细目”的代码。这真正是“接口与实现分离”!
这个分离的关键在于以”声明的依存性“替换”定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。
此时,Person 两个成员函数的实现如下:
#include "Person.h"
# PersonImpl和Person的成员函数完全相同,两者的接口也完全相同
#include "PersonImpl.h"
Person::Person(const std::string& name, const Date& birthday, )
const Address& addr:pImpl(new PersonImpl(name, birthday, addr)) {}
std::string Person::name() const
{
return pImpl->name();
}
上述代码,让 Person 变成一个 Handle class 并不会改变它做的事,只会改变它做事的方法。
解决方式二:Interface classes
令 Person 成为接口类,其目的是详细地描述 derived classes 的接口,因此它通常不带成员变量,也没有构造函数,只有一个 virtual 析构函数以及一组 pure virtual 函数来叙述整个接口。一个针对 Person 而写的接口类看起来像下面这样:
class Person {
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() cosnt = 0;
virtual std::string address() const = 0;
...
};
客户要使用这个类的功能必须通过 Person 的 pointers 和 references。就像上面 Handle classes 的客户一样,除非 Interface classes 的接口被修改,否则其客户不需要重新编译。
Interface class 的客户必须有办法为这种 class 创建新对象。他们通常调用一个 factory(工厂)函数或 virtual 构造函数,他们返回指针,指向动态分配所得的对象,而该对象支持 Interface class 的接口。这样的函数在 Interface class 内往往被声明为 static:
class Person {
public:
...
// 返回一个tr1::shared_ptr,指向一个新的Person,并以给定的参数初始化
static std::tr1::shared_ptr<Person> create(
const std::string& name,
const Data& birthday,
const Address& addr);
...
};
其 derived class 的实现如下:
class RealPerson: public Person {
public:
RealPerson(const std::string& name, const Date& birthday,
const Address& addr)
:theName(name), theBirthDate(birthday), theAddress(addr) {}
virtual ~RealPerson() { }
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
Person 类中 create 实现方式如下:
// create的实现
std::tr1::shared_ptr<Person> Person::create(const std::string& name,
const Date& birthday, const Address& addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
客户则以下面的方式使用它们:
std::string name;
Date dateOfBirth;
Address address;
...
// 创建一个对象,支持Person接口
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
std::cout << pp->name()
<< " was born on "
<< pp->birthDate()
<< " and now lives at "
<< pp->address();
...
一个更现实的 Person::create 实现代码会创建不同类型的 derived class 对象,取决于诸如额外参数值、读自文件或数据库的数据、环境变量等等。
Interface calss 有两个常见的实现机制,上述的 RealPerson 是其中之一,另一种实现机制采用多重继承(见条款40)。
评论区