首页 > 代码库 > Effective C++:条款31:将文件间的编译依存关系将至最低

Effective C++:条款31:将文件间的编译依存关系将至最低

(一)

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