23-4 关联
在前两节课中,我们探讨了两种对象组合形式:组合与聚合。对象组合用于建模复杂对象由一个或多个简单对象(部件)构成的关系。
本节课我们将研究两种原本无关对象之间较弱的关系类型,称为关联。与对象组合关系不同,关联中不存在隐含的整体/部件关系。
关联
要构成关联关系,两个对象必须满足以下条件:
- 关联对象(成员)与关联对象(类)之间不存在其他关联
- 关联对象(成员)可同时属于多个对象(类)
- 关联对象(成员)的存在不受关联对象(类)管理
- 关联对象(成员)可能知晓也可能不知晓对象(类)的存在
与构成或聚合不同——其中部分是整体对象的组成部分——在关联中,关联对象与对象之间不存在其他关联。如同聚合,关联对象可同时属于多个对象,且不受这些对象管理。但与聚合关系始终单向不同,关联关系可为单向或双向(即两个对象相互感知)。
医生与患者的关系是关联的典型示例。医生显然与其患者存在关联,但概念上并非部分/整体(对象组合)关系。医生每日可接诊多位患者,患者亦可就诊多位医生(例如寻求第二诊疗意见或咨询不同专科)。两类对象的生命周期彼此独立。
关联关系可视为“使用”关系:医生“使用”患者(获取收入),患者使用医生(满足各类医疗需求)。
实现关联关系
由于关联关系属于广泛的关系类型,其实现方式多种多样。但最常见的做法是使用指针实现关联关系,即对象指向关联对象。
本例将实现双向的医生/患者关联关系,因为医生需要了解其患者信息,反之亦然。
#include <functional> // reference_wrapper
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
// Since Doctor and Patient have a circular dependency, we're going to forward declare Patient
class Patient;
class Doctor
{
private:
std::string m_name{};
std::vector<std::reference_wrapper<const Patient>> m_patient{};
public:
Doctor(std::string_view name) :
m_name{ name }
{
}
void addPatient(Patient& patient);
// We'll implement this function below Patient since we need Patient to be defined at that point
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_doctor{}; // so that we can use it here
// We're going to make addDoctor private because we don't want the public to use it.
// They should use Doctor::addPatient() instead, which is publicly exposed
void addDoctor(const Doctor& doctor)
{
m_doctor.push_back(doctor);
}
public:
Patient(std::string_view name)
: m_name{ name }
{
}
// We'll implement this function below to parallel operator<<(std::ostream&, const Doctor&)
friend std::ostream& operator<<(std::ostream& out, const Patient& patient);
const std::string& getName() const { return m_name; }
// We'll friend Doctor::addPatient() so it can access the private function Patient::addDoctor()
friend void Doctor::addPatient(Patient& patient);
};
void Doctor::addPatient(Patient& patient)
{
// Our doctor will add this patient
m_patient.push_back(patient);
// and the patient will also add this doctor
patient.addDoctor(*this);
}
std::ostream& operator<<(std::ostream& out, const Doctor& doctor)
{
if (doctor.m_patient.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_patient)
out << patient.get().getName() << ' ';
return out;
}
std::ostream& operator<<(std::ostream& out, const Patient& patient)
{
if (patient.m_doctor.empty())
{
out << patient.getName() << " has no doctors right now";
return out;
}
out << patient.m_name << " is seeing doctors: ";
for (const auto& doctor : patient.m_doctor)
out << doctor.get().getName() << ' ';
return out;
}
int main()
{
// Create a Patient outside the scope of the Doctor
Patient dave{ "Dave" };
Patient frank{ "Frank" };
Patient betsy{ "Betsy" };
Doctor james{ "James" };
Doctor 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;
}
这将输出:

通常情况下,如果单向关联能够满足需求,就应避免使用双向关联,因为双向关联会增加复杂性,且更容易在编写过程中出现错误。
自反关联
有时对象可能与同类型的其他对象存在关联关系,这称为自反关联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建立单向关联,而无需实际包含Car指针或引用成员:
#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; }
};
// Our CarLot is essentially just a static array of Cars and a lookup function to retrieve them.
// Because it's static, we don't need to allocate an object of type CarLot to use it
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{}; // we're associated with the Car by ID rather than pointer
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 is driving the car with ID 17
Car* car{ CarLot::getCar(d.getCarId()) }; // Get that car from the car lot
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并不持有指向其汽车的指针——而是持有汽车的ID,当需要时,我们可以使用该ID从CarLot中获取汽车。
在这个特定示例中,这种做法有些愚蠢,因为从CarLot中获取汽车需要低效的查找(使用指针连接两者会快得多)。然而,使用唯一ID而非指针来引用事物也有其优势。例如,你可以引用当前不在内存中的事物(它们可能位于文件或数据库中,并可在需要时加载)。此外,指针占用4或8字节空间——若内存资源紧张且唯一对象数量较少,使用8位或16位整数引用可大幅节省内存。
组合、聚合与关联的区别总结
以下总结表可帮助您区分组合、聚合与关联的差异:
| Property | Composition | Aggregation | Association |
|---|---|---|---|
| Relationship type | Whole/part | Whole/part | Otherwise unrelated |
| Members can belong to multiple classes | No | Yes | Yes |
| Members’ existence managed by class | Yes | No | No |
| Directionality | Unidirectional | Unidirectional | Unidirectional or bidirectional |
| Relationship verb | Part-of | Has-a | Uses-a |

浙公网安备 33010602011771号