在前两节课程中,我们讨论了两种对象组合——组合(composition)与聚合(aggregation)。对象组合用于刻画“复杂对象由一个或多个简单对象(部件)构成”的关系。
本节课程将探讨一种更为松散的对象间关系,称为关联(association)。与对象组合不同,关联不隐含“整体/部分”关系。
关联
要成为关联,两个对象之间必须满足以下条件:
- 被关联对象(成员)与当前对象(类)在其他方面彼此独立;
- 被关联对象(成员)可同时隶属于多个对象(类);
- 被关联对象(成员)的生命周期不由当前对象(类)管理;
- 被关联对象(成员)可以知道,也可以不知道当前对象(类)的存在。
与组合或聚合相比,关联中的对象并非“部分”与“整体”的关系;如同聚合,被关联对象可同时属于多个对象且不受管理;但与聚合不同,聚合总是单向,而关联既可是单向,亦可是双向(两对象彼此感知)。
医生与患者的关系便是关联的典型例子。医生与患者之间显然存在关系,但概念上并非“整体/部分”。一名医生一天可看多位患者,一名患者也可就诊于多位医生(求第二意见或看不同专科)。两对象的生命周期互不依赖。
我们可说,关联刻画“uses-a”关系:医生“使用”患者(获得收入),患者“使用”医生(获得医疗服务)。
实现关联
关联是宽泛的关系类型,实现方式多样,但最常用的是指针:对象通过指针指向被关联对象。
以下示例实现双向 Doctor/Patient 关系:医生需知道患者,患者也需知道医生。
#include <functional> // reference_wrapper
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
// 前向声明,解决循环依赖
class Patient;
class Doctor
{
private:
std::string m_name{};
std::vector<std::reference_wrapper<const Patient>> m_patients{};
public:
Doctor(std::string_view name) : m_name{ name } {}
void addPatient(Patient& patient);
// 由于需要 Patient 定义,实现在 Patient 类后
friend std::ostream& operator<<(std::ostream& out, const Doctor& doctor);
const std::string& getName() const { return m_name; }
};
class Patient
{
private:
std::string m_name{};
std::vector<std::reference_wrapper<const Doctor>> m_doctors{};
// 设为私有,禁止外部直接调用,应通过 Doctor::addPatient()
void addDoctor(const Doctor& doctor)
{
m_doctors.push_back(doctor);
}
public:
Patient(std::string_view name) : m_name{ name } {}
friend std::ostream& operator<<(std::ostream& out, const Patient& patient);
const std::string& getName() const { return m_name; }
// 允许 Doctor::addPatient 访问私有 addDoctor
friend void Doctor::addPatient(Patient& patient);
};
void Doctor::addPatient(Patient& patient)
{
m_patients.push_back(patient);
patient.addDoctor(*this);
}
std::ostream& operator<<(std::ostream& out, const Doctor& doctor)
{
if (doctor.m_patients.empty())
{
out << doctor.m_name << " has no patients right now";
return out;
}
out << doctor.m_name << " is seeing patients: ";
for (const auto& patient : doctor.m_patients)
out << patient.get().getName() << ' ';
return out;
}
std::ostream& operator<<(std::ostream& out, const Patient& patient)
{
if (patient.m_doctors.empty())
{
out << patient.getName() << " has no doctors right now";
return out;
}
out << patient.m_name << " is seeing doctors: ";
for (const auto& doctor : patient.m_doctors)
out << doctor.get().getName() << ' ';
return out;
}
int main()
{
Patient dave{ "Dave" }, frank{ "Frank" }, betsy{ "Betsy" };
Doctor james{ "James" }, scott{ "Scott" };
james.addPatient(dave);
scott.addPatient(dave);
scott.addPatient(betsy);
std::cout << james << '\n';
std::cout << scott << '\n';
std::cout << dave << '\n';
std::cout << frank << '\n';
std::cout << betsy << '\n';
return 0;
}
输出:
James is seeing patients: Dave
Scott is seeing patients: Dave Betsy
Dave is seeing doctors: James Scott
Frank has no doctors right now
Betsy is seeing doctors: Scott
一般而言,若单向关联即可满足需求,应避免双向关联,以降低复杂性并减少出错可能。
自反关联(Reflexive association)
有时对象与同类型的其他对象产生关系,称为自反关联。例如大学课程与其先修课(也是课程)。
简化示例:每门课最多一个先修课:
#include <string>
#include <string_view>
class Course
{
private:
std::string m_name{};
const Course* m_prerequisite{};
public:
Course(std::string_view name, const Course* prerequisite = nullptr)
: m_name{ name }, m_prerequisite{ prerequisite } {}
};
可形成链式关联:课程 → 先修课 → …
关联可间接实现
前述示例均用指针或引用直接连接对象,但关联并不限于此。任何能唯一标识并链接两对象的数据均可。
下例展示 Driver 与 Car 的单向关联,但 Driver 并不持有 Car 指针,而是通过 ID 间接关联:
#include <iostream>
#include <string>
#include <string_view>
class Car
{
private:
std::string m_name{};
int m_id{};
public:
Car(std::string_view name, int id) : m_name{ name }, m_id{ id } {}
const std::string& getName() const { return m_name; }
int getId() const { return m_id; }
};
namespace CarLot
{
Car carLot[4]{
{ "Prius", 4 }, { "Corolla", 17 }, { "Accord", 84 }, { "Matrix", 62 }
};
Car* getCar(int id)
{
for (auto& car : carLot)
if (car.getId() == id) return &car;
return nullptr;
}
}
class Driver
{
private:
std::string m_name{};
int m_carId{}; // 用 ID 而非指针关联 Car
public:
Driver(std::string_view name, int carId) : m_name{ name }, m_carId{ carId } {}
const std::string& getName() const { return m_name; }
int getCarId() const { return m_carId; }
};
int main()
{
Driver d{ "Franz", 17 }; // Franz 驾驶 ID=17 的车
Car* car = CarLot::getCar(d.getCarId());
if (car)
std::cout << d.getName() << " is driving a " << car->getName() << '\n';
else
std::cout << d.getName() << " couldn't find his car\n";
return 0;
}
本例中 CarLot 静态保存所有汽车。Driver 并不直接持有 Car*,而是保存 ID,需要时通过 ID 从 CarLot 获取对应 Car。
此做法看似低效(需遍历查找),但用 ID 而非指针有其优势:
- 可引用当前不在内存的对象(如存于文件或数据库,按需加载);
- 若对象数量不多且内存紧张,用 8 位或 16 位整数代替 4/8 字节指针可大幅节省空间。
组合 vs 聚合 vs 关联 一览
下表助记三者差异:
属性 | 组合 (Composition) | 聚合 (Aggregation) | 关联 (Association) |
---|---|---|---|
关系类型 | 整体/部分 | 整体/部分 | 彼此独立 |
成员可属多对象 | 否 | 是 | 是 |
成员生命周期由类管 | 是 | 否 | 否 |
方向性 | 单向 | 单向 | 单向或双向 |
关系动词 | part-of | has-a | uses-a |