我们之前的 Item(如 32, 34, 36)都在讨论 Public Inheritance(公有继承),它的核心意义是 "Is-a"(是一个) 的关系。 而 Composition(复合/组合) ——即一个类包含另一个类型的对象作为成员变量——则代表了完全不同的两种意义:
- Has-a(拥有):应用域(Application Domain)的概念。
- Is-implemented-in-terms-of(根据某物实现出):实现域(Implementation Domain)的概念。
以下是深度解析。
1. 什么是复合 (Composition)?
非常简单,就是一个类里面有另一个类的对象。
class Address { ... };
class PhoneNumber { ... };class Person {
public:...
private:std::string name; // CompositionAddress address; // CompositionPhoneNumber voiceNumber; // CompositionPhoneNumber faxNumber; // Composition
};
在这个例子中,Person 是由 string, Address, PhoneNumber 等对象组成的。这就是复合。
2. 第一层含义:Has-a(拥有)
这是最直观的含义,通常出现在应用域(即你正在建模的现实世界业务逻辑)中。
- 逻辑: 人(Person)拥有一个名字(Name)。人拥有一个地址(Address)。
- 反例: 你绝不会说“人是一个地址”(Person is an Address)。因此,
Person不应该继承Address,而应该包含它。
这一点大多数人都不会搞错。
3. 第二层含义:Is-implemented-in-terms-of(根据某物实现出)
这是实现域(即纯粹为了写代码、数据结构、算法)中的概念。这也是最容易误用继承的地方。
场景案例:我们需要一个 Set(集合)
假设你需要实现一个 Set 模板类。你知道 Set 的特性是:
- 元素不能重复。
- 通常无序(或者是特定的数学顺序)。
- 需要空间存储元素。
你手里正好有一个现成的、功能强大的 std::list。你想:“太好了,我可以复用 std::list 的代码来存储数据。”
❌ 错误的各种做法:使用 Public Inheritance
// 错误设计:让 Set 继承 List
template<typename T>
class Set : public std::list<T> { ... };
这就犯了 Item 32 ("Public inheritance means is-a") 的大忌。 如果 Set 继承了 List,那么 Set 就是一个 List。这就意味着适用于 List 的所有操作必须适用于 Set。
List可以包含重复元素。Set不行。List可以在指定位置插入 (insert)。Set通常不需要关心物理位置。List可以拼接 (splice)。
如果用户写了这行代码:
Set<int> s;
s.push_back(10); // 第一次
s.push_back(10); // 第二次!List 允许这样做,但 Set 不应该允许!
此时你的 Set 就破功了。因为它继承了 List 的接口,导致它的行为不再像一个 Set。
✅ 正确的做法:使用 Composition
我们应该说:Set 是根据 List 实现出来的(Set is implemented in terms of List)。 Set 使用 List 来管理内存和数据,但 Set 不是 一个 List。
template<typename T>
class Set {
public:// 只有 Set 该有的接口bool member(const T& item) const;void insert(const T& item);void remove(const T& item);std::size_t size() const;private:// 内部实现细节:使用 list 来干活std::list<T> rep;
};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); // 复用 list 的功能}
}
为什么这样更好?
- 接口隔离:
Set的用户看不到std::list的push_front、splice等不适合Set的函数。 - 封装性:以后如果你发现
std::list性能不好,想换成std::vector或自定义的哈希表,你只需要修改Set的内部实现 (private部分),外部使用Set的代码完全不需要改动。
4. 总结
- Public 继承:意味着 "Is-a"(是一个)。除此之外,别用它。
- Composition(复合):意味着 "Has-a"(拥有)或 "Is-implemented-in-terms-of"(根据某物实现出)。
- 如果是现实世界的对象关系(如人与地址),它是 Has-a。
- 如果是纯粹的代码复用(如用 List 实现 Set),它是 Is-implemented-in-terms-of。
在设计类的关系时,优先考虑组合,而不是继承。