复合(composition)是类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系。例如:
class Address { ... }; // 某人的住址
class PhoneNumber { ... };
class Person {
public:
...
private:
std::string name; // 合成成分物(composed object)
Address address; // 同上
PhoneNumber voiceNumber; // 同上
PhoneNumber faxNumber; // 同上
};
本例之中Person对象由string, Address, PhoneNumber构成。在程序员之间复合(composition)这个术语有许多同义词,包括layering(分层),containment(内含),aggregation(聚合)和embedding(内嵌)。
条款32曾说,“public继承”带有is-a(是一种)的意义。复合也有它自己的意义。实际上它有两个意义。复合意味着has-a(有一个)或is-implemented-in-terms-of(根据某物实现出)。那是因为你打算在你的软件中处理两个不同的领域(domains)。程序中的对象其实相当于你所塑造的世界中的某些事物,例如人、汽车、一张张视频画面等等。这样的对象属于应用域(application domains)部分。其它对象则纯粹是实现细节上的人工制品,像是缓冲区(buffers)、互斥器(mutexes)、查找树(search trees)等等。这些对象相当于你的软件的实现域(implementation domain)。当复合发生于应用域内的对象之间,表现出has-a的关系;当它发生于实现域内则是表现is-implemented-in-terms-of的关系。
上述的Person class示范has-a关系。Person有一个名称,一个地址,以及语音和传真两笔电话号码。你不会说“人是一个名称”或“人是一个地址”,你会说“人有一个名称”和“人有一个地址”。大多数人接受此一区别毫无困难,所以很少人会对is-a和has-a感到困惑。
比较麻烦的是区分is-a(是一种)和is-implemented-in-terms-of(根据某物实现出)这两种对象的关系。假设你需要一个template,希望制造出一组classes用来表现由不重复对象组成的sets。由于复用(reuse)是件美妙无比的事情,你的第一个直觉是采用标准程序库提供的set template。是的,如果他人所写的template合乎需求,我们何必另写一个呢?
不幸的是set的实现往往招致“每个元素耗用三个指针”的额外开销。因为sets通常以平衡查找树(balanced search trees)实现而成,使它们在查找、插入、移除元素时保证拥有对数时间(logarithmic-time)效率。当速度比空间重要,这是个通情达理的设计,但如果你的程序却是空间比速度重要呢?那么标准程序库的set提供给你的是个错误决定下的取舍。似乎你终究还得写个自己的template。
当容我再说一次,复用(reuse)是件美好的事。如果你是一位数据结构专家,你就会知道,实现sets的方法太多了,其中一种便是在底层采用linked lists。而你又刚好知道,标准程序库有一个list template,于是你决定复用它。
更明确地说,你决定让你那个萌芽中的Set template继承std::list。也就是让Set<T>继承list<T>。毕竟在你的实现理念中Set对象其实是个list对象。你于是声明Set template如下:
template<typename T> // 将list应用于Set。错误做法。
class Set: public std::list<T> { ... };
每件事看起来都很好,但实际上有些东西完全错误。一如条款32所说,如果D是一种B,对B为真的每一件事情对D也都应该为真。但list可以内含重复元素,如果数值3051被安插到Set<int>两次,这个Set只内含一笔3051.因此“Set是一种list”并不为真,因为对list对象为真的某些事情对Set对象并不为真。
由于这两个classes之间并非is-a的关系,所以public继承不适合用来塑模它们。正确的做法是,你应当了解,Set对象可根据一个list对象实现出来:
template<class T> // 将list应用于Set。正确做法
class Set {
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep; // 用来表述Set的数据
};
Set成员函数可大量依赖list及标准程序库其它部分提供的机能来完成,所以其实现很直观也很简单,只要你熟悉以STL编写程序:
template<typename T>
bool Set<T>::member(const T& item) const
{
return std::find(rep.begin(), rep.end(), item) != rep.end();
}
template<typename T>
void Set<T>::insert(const T& item)
{
if (!member(item)) {
rep.push_back(item);
}
}
template<typename T>
void Set<T>::remove(const T& item)
{
typename std::list<T>::iterator it = std::find(rep.begin(), rep.end(), item); // 见条款42对“typename”的讨论
if (it != rep.end()) {
rep.erase(it);
}
}
template<typename T>
std::size_t Set<T>::size() const
{
return rep.size();
}
这些函数如此简单,因此都适合成为inlining候选人。但请记住,在做出任何与inlining有关决定之前,应该先看看条款30。
请记住
- 复合(composition)的意义和public继承完全不同。
- 在应用域(application domain),复合意味has-a(有一个)。在实现域(implementation domain),复合意味is-implemented-in-terms-of(根据某物实现出)。