侧边栏壁纸
博主头像
如此肤浅

但行好事,莫问前程!

  • 累计撰写 52 篇文章
  • 累计创建 6 个标签
  • 累计收到 6 条评论

目 录CONTENT

文章目录

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

如此肤浅
2022-06-06 / 0 评论 / 0 点赞 / 25 阅读 / 3,833 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-06-06,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

1. 支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是 Handle classes 和 Interface classes。
2. 程序库头文件应该以”完全且仅有声明式”(full and declaration-only forms)的形式存在。这种做法不论是否涉及 templates 都适用。

问题描述

假设你对 C++ 程序的某个 class 实现文件做了些轻微修改。注意,修改的不是 class 接口,而是实现,而且只改 private 成分。然后重新构建这个程序,但你会发现这可能需要很长时间进行编译链接。

问题出在 C++ 并没有把”将接口从实现中分离“这事做得很好。Class 的定义式不只详细叙述了 class 接口,还包括许多的实现细节,例如:

#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 文件和这些含入的文件之间形成了一种编译依存关系。如果这些头文件中有任何一个被改变,或这些头文件所倚赖的其他头文件有任何改变,那么每一个含入 Person class 的文件就得重新编译,任何使用 Person class 的文件也必须重新编译。这样的连串编译依存关系会对许多项目造成难以形容的灾难。

解决方式一:Handle classes

因此,针对 Person 我们可以这样做:把 Person 分割成两个类,一个只提供接口,另一个负责实现该接口。如果负责设计的那个所谓的实现类取名为 PersonImpl,那么 Person 将定义如下:

#include <string>
#include <memory> // 为了使用tr1::shared_ptr而含入

class PersonImpl; // Person实现类的前置声明
class Date; // Person接口用到的classes的前置声明
class Address;

class Person {
public:
	Person(const std::string& name, const Date& birthday,
    		const Address& addr);
	std::string name() const;
    std::string birthDate() const;
    std::string addres() const;
    ...
private:
	std::tr1::shared_ptr<PersonImpl> pImpl; // 指针,指向实现物
};

在这里, Person 只内含一个指针成员(这里使用tr1::shared_ptr,见条款13),指向其实现类(PersonImpl)。这样的设计常被称为”pointer to implementation“。这种 classes 内的指针名称往往就是 pImpl,就像上面代码那样。

在这样的设计下,Person 的客户就完全与 Dates、Addresses 以及 Persons 的实现细节分离了。那些 classes 的任何实现修改都不需要 Person 客户端重新编译。此外,由于客户无法看到 Person 的实现细目,也就不可能写出什么”取决于那些细目”的代码。这真正是“接口与实现分离”!

这个分离的关键在于以”声明的依存性“替换”定义的依存性”,那正是编译依存性最小化的本质:现实中让头文件尽可能自我满足,万一做不到,则让它与其他文件内的声明式(而非定义式)相依。

此时,Person 两个成员函数的实现如下:

#include "Person.h"
# PersonImpl和Person的成员函数完全相同,两者的接口也完全相同
#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();
}

上述代码,让 Person 变成一个 Handle class 并不会改变它做的事,只会改变它做事的方法。

解决方式二:Interface classes

令 Person 成为接口类,其目的是详细地描述 derived classes 的接口,因此它通常不带成员变量,也没有构造函数,只有一个 virtual 析构函数以及一组 pure virtual 函数来叙述整个接口。一个针对 Person 而写的接口类看起来像下面这样:

class Person {
public:
	virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() cosnt = 0;
    virtual std::string address() const = 0;
    ...
};

客户要使用这个类的功能必须通过 Person 的 pointers 和 references。就像上面 Handle classes 的客户一样,除非 Interface classes 的接口被修改,否则其客户不需要重新编译。

Interface class 的客户必须有办法为这种 class 创建新对象。他们通常调用一个 factory(工厂)函数或 virtual 构造函数,他们返回指针,指向动态分配所得的对象,而该对象支持 Interface class 的接口。这样的函数在 Interface class 内往往被声明为 static:

class Person {
public:
	...
    // 返回一个tr1::shared_ptr,指向一个新的Person,并以给定的参数初始化
    static std::tr1::shared_ptr<Person> create(
    	const std::string& name,
    	const Data& birthday,
        const Address& addr);
	...
};

其 derived class 的实现如下:

class RealPerson: public Person {
public:
	RealPerson(const std::string& name, const Date& birthday, 
    			const Address& addr)
    	:theName(name), theBirthDate(birthday), theAddress(addr) {}
	virtual ~RealPerson() { }
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
	std::string theName;
    Date theBirthDate;
    Address theAddress;
};

Person 类中 create 实现方式如下:

// create的实现
std::tr1::shared_ptr<Person> Person::create(const std::string& name,
	const Date& birthday, const Address& addr)
{
	return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}

客户则以下面的方式使用它们:

std::string name;
Date dateOfBirth;
Address address;
...
// 创建一个对象,支持Person接口
std::tr1::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
...
std::cout << pp->name()
	<< " was born on "
    << pp->birthDate()
    << " and now lives at "
    << pp->address();
...

一个更现实的 Person::create 实现代码会创建不同类型的 derived class 对象,取决于诸如额外参数值、读自文件或数据库的数据、环境变量等等。

Interface calss 有两个常见的实现机制,上述的 RealPerson 是其中之一,另一种实现机制采用多重继承(见条款40)。

0

评论区