前言
假如你对C++程序的某个class实现文件做了轻微的修改。注意,修改的不是class接口,而是实现,而且只改private部分。然后重新编译整个程序,预计只花几秒就好,结果你发现得花费数倍这个时间,仿佛整个程序重新编译了!当你碰到这种事情的时候,你不想骂人吗?
让我们来看以下的代码
#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 class的文件就的重新编译,任何使用Person class的文件也必须重新编译。这样的连串编译依存关系会对许多项目造成难以形容的灾难。
为什么会出现这种情况?
因为编译器在编译期间需要知道对象的大小,当编译器看到Person对象,它获取这项信息的唯一办法就是询问class定义式,所以如果你尝试把#include "date.h"和#include “address.h”删掉,你会发现程序根本编译不过。还好C++有前置声明,帮我们很好的解决这个问题,往下看。
正确的做法
把Person分割成两个classes,一个只提供接口,另一个负责实现该接口。负责实现的那个implementation class取名PersonImpl
// Person.h
#include <string>
#include <memory> // shared_ptr需要包含的头
class PersonImpl; // Person实现类的前置声明
class Date; // Person接口用到的classes的前置声明
class Address;
class Person {
public:
Person(const std::string &name, Date* birthday, Address* addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl; // 智能指针,防止内存泄漏
}
// Person.cpp
std::string Person::name() const
{
return pImpl->name();
}
std::string Person::birthDate() const
{
return pImpl->birthDate()
}
std::string Person::address() const
{
return pImpl->address();
}
优点
这样的设计之下,Person的客户就完全与Dates,Addresses以及Persons的实现细目分离了。那些classes的任何实现修改都不需要Person客户端重新编译。此外由于客户无法看到Person的实现细目,也就不可能写出什么“取决于那些细目”的代码,这是真正的"接口与实现分离"。
设计策略
- 如果使用对象引用或对象指针可以完成任务,就不要使用对象。
- 如果能够,尽量以class声明式替代class定义式。