(一)
假设你对C++程序的某个class实现文件做了些轻微改变,修改的不是接口,而是实现,而且只改private成分。然后重新建置这个程序,并预计只花数秒就好,当按下“Build”或键入make,会大吃一惊,因为你意识到整个世界都被重新编译和链接了!问题是在C++并没有把“将接口从实现中分离”做得很好。
避免陷入这种窘境的一种有效的方法就是本条款要提出的内容:将文件间的编译依存关系降至最低.
(二)
首先有下面这样的代码:
class Person {
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
string name() const;
string birthDate() const;
string address() const;
private:
string theName; //实现细目
Date theBirthDate; //实现细目
Address theAddress; //实现细目
};
这样写显然在Person定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency).可能就会导致开头我们提到的使你陷入窘境的情形出现。
(三)解决办法
(1)第一种办法:Handle class
所以这里我们采取了另外一种实现方式,即将对象实现细则隐藏与一个指针背后.具体这样做:把Person类分割为两个类,一个只提供接口,另一个负责实现该接口。
把Person分割为两个classes,一个提供接口,另一个负责实现接口。负责实现的那个所谓的implementation class取名为PersonImpl。
#include <string>
#include <memory>
class PersonImpl;
class Date;
class Address;
class Person {
public:
Person(const string& name, const Date& birthday, const Address& addr);
string name() const;
string birthDate() const;
string address() const;
private:
tr1::shared_ptr<PersonImpl> pImpl;
};
这里,Person只内含一个指针成员,指向其实现类(PersonImpl)。这个设计常被称为pimpl idiom(pimpl是“pointer to implementation”的缩写)。这样,Person的客户就完全与Date,Address以及Person的实现细目分离了。那些classes的任何实现修改都不需要Person客户端重新编译。
这种使用pimpl idiom的classes,往往被称为Handle classes。
#include "Person.h"
#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();
}
(2)第二种办法: Interface classes(abstract base class(抽象基类))
这种class的目的是详细一一描述derived classes的接口,因此它通常不带成员变量,也没有构造函数,只有一个virtual析构函数以及一组pure virtual函数,又来叙述整个接口。
一个针对Person而写的Interface class或许看起来像这样:
class Person {
public:
virtual ~Person();
virtual string name() const = 0;
virtual string birthday() const = 0;
virtual string address() const = 0;
};
该Person类不能被实例化,所以这个class的客户必须以Person的pointers和reference来撰写应用程序,不能针对“内含pure virtual函数”的Person classes具现出实体。除非Interface class的接口被修改否则其客户不需要重新编译。
所以我们通过工厂函数来产生该 Person类的 pointers或reference来撰写应用程序:
class Person {
public:
static tr1::shared_ptr<Person> create(const string& name, const Date& birthday, const Address& addr);
};
客户可能会这样使用它们:
string name;
Date dateBirth;
Address address;
tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address));
...
std::cout << pp->name()
<< "was born on "
<< PP->birthDate()
<< " and now lives at "
<< pp->address();
当然支持interface class接口的那个具象类(concrete classes)必须被定义出来,而真正的构造函数必须被调用。
假设有个derived class RealPerson,提供继承而来的virtual函数的实现:
class RealPerson : public Person {
public:
RealPerson(const std::string& name, const Date& birthday, const Address& addr)
: theName(name), theBirthDate(birthday), theAddress(addr)
{ }
virtual ~RealPerson(){}
string name() const;
string birthDate() const;
string address() const;
private:
string theName;
Date theBirthDate;
Address theAddress;
};
有了RealPerson之后,写出Person::create就真的一点也不稀奇了:
tr1::shared_ptr<Person> Person::create(const string& name, const Date& birthday, const Address& addr) {
return tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
RealPerson示范实现了Interface class的两个最常见机制之一:从interface class继承接口规格,然后实现出接口所覆盖的函数。
(四)
handle classes 和 interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性。
在程序开发过程中使用handle class 和 interface class以求实现码有所改变时对其客户带来最小冲击。
两种class的实现方案带来的运行成本也是不容忽视的。
支持“编译依存最小化”的一般构想是:相依于声明式,不要相依于定义式。
请记住:
(1)支持"编译依存性最小化"的一般构想是:相依于声明式,而不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes。(2)程序库头文件应该以"完全且仅有的声明式"的形式存在.这种做法不论是否涉及templates都适用。