关联
本节阅读量:
在前两节课中,我们讨论了两种对象关系:组合和聚合。在这两种关系中,复杂对象(整体)由一个或多个简单对象(部分)构建而成。
在本课中,我们将研究两个原本不相关的对象之间一种较弱的关系,称为关联。与对象组合关系不同,关联中没有隐含的整体/部分关系。
关联
要符合关联的条件,对象和另一个对象必须具有以下关系:
- 被关联的对象(成员)与当前对象(类)无整体/部分的关系
- 被关联的对象(成员)可以同时属于多个对象(类)
- 被关联的对象(成员)的生命周期不被当前对象(类)管理
- 被关联的对象(成员)可能也可能不知道当前对象(类)的存在
组合或聚合表达的是整体与部分的关系。对于关联而言,被关联的对象之间没有这种关系。与聚合类似,被关联的对象可以同时属于一个或多个对象,但它不由这些对象管理。对于聚合而言,关系是单向的,即一个对象是整体,另一个对象是部分。关联关系则可能是双向的,两个对象可以互相感知到对方的存在。
医生和患者之间是一种关联关系。医生显然与他的病人有关,但从概念上讲,这不是一种部分/整体(对象构成)关系。医生一天可以看许多患者,患者也可以看许多医生(也许他们需要第二诊疗意见,或者正在就诊于不同类型的医生)。任一对象的生命周期都不与另一个对象绑定。
我们可以说,关联模型是“使用”关系。医生“使用”病人(赚取收入)。患者“使用”医生(治疗疾病)。
实现关联
关联是一种广泛存在的关系,可以用许多不同的方法来实现。然而,在大多数情况下,关联是通过指针实现的,即当前对象指向被关联的对象。
在本例中,我们将实现医生/患者的双向关系,因为医生知道自己的患者是谁是有意义的,反过来也一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
#include <functional> // reference_wrapper
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
// 因为 Doctor 和 Patient 循环依赖, 所以需要前向声明 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);
// 这个函数需要在 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_doctor{}; // 这里记录看过的医生
// 这个函数设置为private,因为不想它被任意调用
// 需要使用 Doctor::addPatient(), 然后在其中调用本函数
void addDoctor(const Doctor& doctor)
{
m_doctor.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() 为友元函数,以便Doctor可以访问private函数 Patient::addDoctor()
friend void Doctor::addPatient(Patient& patient);
};
void Doctor::addPatient(Patient& patient)
{
// 这里doctor遇到patient
m_patient.push_back(patient);
// patient 也同样遇到 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()
{
// Patient 对象的创建在 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;
}
|
打印
1
2
3
4
5
|
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
|
一般来讲,如果单向关联已经能满足逻辑需求,就应该避免双向关联。因为双向关联会增加复杂性,往往很难在不出错的情况下完成实现。
自反关联
有时,对象可能与相同类型的其它对象存在关系。这被称为自反关联。自反关联的一个很好例子是大学课程与先修课程之间的关系。
考虑一个简化的情况,一门课程只能有一个先修课程。可以这样做:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#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 }
{
}
};
|
这可能导致一系列关联(A课程有一个先修课程B,B课程有一个先修课程C,等等……)
间接关联
关联可以是间接的。在前面的所有示例中,我们都使用指针或引用来直接将对象链接在一起。然而,在关联中,这并不是严格要求。只要某种数据能够把两个对象联系起来,就已经足够了。下面的示例展示了Driver类如何在不实际包含Car指针或引用的情况下,与Car建立单向关联:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
|
#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; }
};
// carLot 是一个静态数组,装载了Car和查找的id
// 因为它是静态的,所以不需要分配一个CarLot对象
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 而不是指针来进行关联
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()) }; // 从 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用于存放所有汽车。需要汽车的驾驶员没有指向自己汽车的指针——相反,他持有汽车的ID,并在需要时使用这个ID从carLot中获取汽车。
在这个特定例子中,这样做有点笨拙,因为从carLot中取出汽车需要低效查找(用指针直接连接两者会快得多)。然而,通过唯一ID而不是指针来引用对象也有好处。例如,您可以引用当前不在内存中的内容(它们可能位于文件或数据库中,并且可以按需加载)。此外,指针会占用4或8个字节——如果内存空间很少,并且唯一对象的数量相对较少,那么通过8位或16位整数引用它们可以节省大量内存。
组合vs聚合vs关联摘要
这里有一个摘要表,帮助您记住组合、聚合和关联之间的区别:
| 属性 |
组合 |
聚合 |
关联 |
| 关系类型 |
部分-整体 |
部分-整体 |
相互关联 |
| 成员可以属于多个类 |
否 |
是 |
是 |
| 成员生命周期由类管理 |
是 |
否 |
否 |
| 关系指向 |
单向 |
单向 |
单向或双向 |
| 关系术语 |
一部分 |
有一个 |
使用一个 |