(一)
假设你对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都适用。