关联
本节阅读量:
在前两节课中,我们讨论了两种类型的对象关系,即组合和聚合。其中复杂对象(整体)是由一个或多个简单对象(部分)构建的。
在本课中,将研究两个原本不相关的对象之间的较弱类型的关系,称为关联。与对象组合关系不同,在关联中,没有隐含的整体/部分关系。
关联
要符合关联的条件,对象和另一个对象必须具有以下关系:
- 被关联的对象(成员)与当前对象(类)无整体/部分的关系
- 被关联的对象(成员)可以同时属于多个对象(类)
- 被关联的对象(成员)的生命周期不被当前对象(类)管理
- 被关联的对象(成员)可能也可能不知道当前对象(类)的存在
对组合或聚合而言,代表的是整体和部分的关系。对于关联而言,关联的对象之间无这种关系。与聚合类似,被关联的对象,可以同时属于一个或多个对象,但是它不归这些对象所管理。对于聚合而言,关系是单向的,即一个对象是整体,一个对象是部分。关联的关系可能是双向的,两个对象可以互相感知到对方的存在。
医生和患者之间的关系是一个关联的关系。医生显然与他的病人有关系,但从概念上讲,这不是一种部分/整体(对象构成)关系。医生一天可以看许多患者,患者可以看许多医生(也许他们需要第二种意见,或者他们正在拜访不同类型的医生)。对象的生命周期都不与另一个对象绑定。
我们可以说,关联模型是“使用”关系。医生“使用”病人(赚取收入)。患者“使用”医生(治疗疾病)。
实现关联
关联是一种广泛存在关系,可以用许多不同的方法来实现它们。然而,大多数情况下,关联是使用指针实现的,其中当前对象指向关联的对象。
在本例中,我们将实现医生/患者双向关系,因为医生知道他们的患者是谁是有意义的,反之亦然。
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,当需要时,可以使用它从carLot中获取汽车。
在这个特定的例子中,这样做有点傻,因为将汽车从carLot中取出需要低效的查找(连接两者的指针要快得多)。然而,通过唯一的ID而不是指针引用事物是有好处的。例如,您可以引用当前不在内存中的内容(可能它们在文件或数据库中,并且可以按需加载)。此外,指针会占用4或8个字节——如果内存空间很少,并且唯一对象的数量相当低,则通过8位或16位整数引用它们可以节省大量内存。
组合vs聚合vs关联摘要
这里有一个摘要表,帮助您记住组合、聚合和关联之间的区别:
属性 |
组合 |
聚合 |
关联 |
关系类型 |
部分-整体 |
部分-整体 |
相互关联 |
成员可以属于多个类 |
否 |
是 |
是 |
成员生命周期由类管理 |
是 |
否 |
否 |
关系指向 |
单向 |
单向 |
单向或双向 |
关系术语 |
一部分 |
有一个 |
使用一个 |