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

条款31:将文件间的编译依存关系降至最低
    (Minimize compilation dependencies between files.)

内容:
    在你们的开发团队中,一些有经验的工程师时不时地会教导新手一些基本的编程原则,其中"将接口从实现中
分离"可能是他(她)要你必须牢记原则,因为C++并没有把它做的很好,这只能靠我们在平时的编写代码中注意这
一点了,如果你不小心违背了这一原则,可能招致的后果就是:当你轻微的修改了某个类的实现,注意不是接口的
时候,再次重新BUILD一次你的工程,Oh,My God!很多文件被重新编译和链接了,Build的时间大大超出你的预期,
而这种事情的发生,估计你当时就会只恨编译器的BUILD的速度太慢,效率太低.呵呵.避免陷入这种窘境的一种有
效的方法就是本条款要提出的内容:将文件间的编译依存关系降至最低.
    现在假设你写了一个Person类,一般你会这么构思你的代码:
    #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定义文件和其含入文件之间形成了一种编译依存关系(compilation dependency).
可能就会导致开头我们提到的使你陷入窘境的情形出现.所以这里我们采取了另外一种实现方式,即将对象
实现细则隐藏与一个指针背后.具体这样做:把Person类分割为两个类,一个只提供接口,另一个负责实现该
接口.
    //person.h
    #include <string>
    #include <memory>
    using std::string;
    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:
        struct Impl;
        std::tr1::shared_ptr<Impl> pImpl_;
    };
    //person.cpp
    #include "person.h"
    struct Person:Impl{
        Impl(const string& name,const Date& birthday,const Address& addr)
            :theName_(name),theBirthDate_(birthday),theAddress_(addr){}
        string name()const{
            return theName_;
        }
        ...
        string theName_;
        Date theBirthDate_;
        Address theAddress_;
    };
    Person::Person(const string& name,const Date& birthday,const Address& addr)
    :pImpl_(new Impl(name,birthday,addr)){
    }
    string Person::name()const{
        return pImpl_->name();
    }
    ...   
    以上这种设计常被称为pimpl idiom("pointer to implementation"),而这种class往往被称为Handle classes
这样任何实现修改都不需要Person客户端的重新编译,此外由于客户无法看到Person的实现细目,也就不可能
写出什么"取决于那些细目"的代码.这是真正的"接口与实现的分离"!这里的关键在于以
"声明的依存性"替换"定义的依存性",那正是编译依存性最小化的本质:现实中让头文
件尽可能的自我满足,万一做不到,则让它与其他文件内的声明式相依.其他每一件事都源自这个简单的设计
策略:
    (1)如果使用object references或object pointers可以完成任务,就不要使用objects.
    (2)如果能够,尽量以class声明式替换class定义式.
    (3)为声明式和定义式提供不同的头文件.(在模板类中常用到.)
    另一个制作Handle class的办法是,令Person成为一种特殊的abstract base class称之为Interface
class.这种class只有一个virtual析构函数以及一组pure virtual函数,用来叙述整个接口.一个针对Person而
写的Interface class或许看起来这样:
    //Person.h
    ...
    using std::string;
    class Date;
    class Address;
    class Person{
    public:
        virtual ~Person();
        virtual string name()const = 0;
        virtual string birthDate()const = 0;
        virtual string address()const = 0;
        ...
        static std::tr1::shared_ptr<Person>
        create(const string& name,const Date& birthday,const Address& addr);
    };
    ...
    //person.cpp
    ...
    class RealPerson:public Person{
    public:
        RealPerson(const string& name,const Date& birthday,const Address& addr);
        virtual ~RealPerson(){}
        string name()const;
        ...
    private:
        string name_;
        Date theBirthDate_;
        Address theAddress_;
    };
    std::tr1::shared_ptr<Person> Person::create(const string& name,
                                                const Date& birthday,
                                                const Address& addr){
        return std::tr1::shared_ptr<Person>(new RealPerson(name,birthday,addr));
    }
    Handle classes和Interface classes解除了接口和实现之间的耦合关系,从而降低文件间的编译依存性.
注意一点,两种class的实现方案带来的运行成本也是不容忽视的(由于篇幅问题,我就不具体阐述,相信你们
也能自己分析的出来).如果你应该从你的实际出发,考虑用渐近方式去使用这些技术.
    请记住:
    ■ 支持"编译依存性最小化"的一般构想是:相依于声明式,而不要相依于定义式.基于此构想的两个
手段是Handle classes和Interface classes.
    ■ 程序库头文件应该以"完全且仅有的声明式"的形式存在.这种做法不论是否涉及templates都适用.

你可能感兴趣的:(条款31:将文件间的编译依存关系降至最低)