组合
概述
在现实生活中,复杂的对象通常是由小的,简单的组成,从简单对象构建复杂对象的过程称为对象组合
例如
- 汽车是用金属框架,发动机,轮胎,变速器和其他大量零件制造而成的
- 个人电脑由CPU,主板,内存等组成
- 即便是你也是由较小部分组成:头,身体,腿,手
从广义上讲,两个对象存在关系构成了对象组合模型
- 汽车有变速箱,你的电脑有CPU,你自己有心脏
- 复杂对象有时被称为整体或父对象,简单的对象通常被称为零件,子零件或组件
在C++中,你已经看到结构和类可以具体各种类型的数据成员(例如基础类型和其他类)
当我们使用数据成员构建类时,实际上是从较简单的部分构造复杂对象,即对象组合,因此,有时将结构和类称为复合类型
对象组合在C++上下文中很有用,因为它使我们可以通过组合更简单,更易于管理的创建复杂的类
这降低了复杂,并且使我们能够更快地编写代码并减少错误,因此我们可以重用已经编写,测试和验证过的有效代码
对象组合的类型
对象组合有两种基本的子类型
- 组合(composition)
- 聚合(aggregation)
关于术语的注释:术语“组合”通常用于指组合和聚合,而不仅指组合
在本篇文章中,当我们提及两者时,将术语“对象组合”,而当具体提及组合,将使用术语组合(composition)
组合
为了符合组合条件,对象和零件必须具有以下关系
- 该部件(成员)是对象(类)的一部分
- 该部件(成员)一次只能属于一个对象(类)
- 该部件(成员)的存在由对象(类)管理
- 该部件(成员)不知道对象(类)的存在
一个真实的组合例子就是一个人的身体和心脏的关系
组合的构成是部件 - 整体的关联,部件是整体的一部分
- 例如,心脏是人的身体的一部分。组合中的部件一次只能是一个对象的一部分
- 属于一个人的身体的心脏不能同时属于另一个人的身体
在组合构建中,对象负责部件的存亡
- 意味着,部件在对象创建的时候创建,并在销毁对象时销毁
- 从更广的范围来说,这意味着,对象不需要用户参与管理部件的生命周期
- 例如,当创建一个身体时,心脏也被创建。当一个人的身体被破坏时,他们的心脏也被破坏。因此,这就构成了“死亡关系”
部件不知道整体的存在
- 你的心脏幸福地运转着,却没有意识到它是更大结构的一部分
- 我们称其为
单向关系
,因此身体了解心脏,但反之未然
部件的可移植说明
- 心脏可以从一个身体移植到另外一个身体
- 移植后仍然符合组合关系的要求(心脏现已由接受者拥有,除非再次移植,否则它只能是接受者的一部分)
例子
class Fraction
{
private:
int m_numerator;
int m_denominator;
public:
Fraction(int numerator=0, int denominator=1):
m_numerator{ numerator }, m_denominator{ denominator }
{
// We put reduce() in the constructor to ensure any fractions we make get reduced!
// Since all of the overloaded operators create new Fractions, we can guarantee this will get called here
reduce();
}
};
- 此类具有两个数据成员:分子和分母。分子和分母是分数的一部件(包含在其中)
- 它们一次不能属于一个以上分数。分子和分母不知道它们是分数的部件,它们知道表示某个整数
- 创建分数实例的时候,同时创建分子和分母两个成员变量。当分数被销毁的时候,分子和分母的成员变量也被销毁
更多例子
#include <iostream>
#include <string.h>
class Point2D
{
private:
int m_x;
int m_y;
public:
// a default constructof
Point2D(): m_x{0}, m_y{0}
{
}
// a specific constructor
Point2D(int x, int y): m_x{x}, m_y{y}
{
}
// an overloaded output operator
friend std::ostream& operator <<(std::ostream& out, const Point2D &point)
{
out << '(' << point.m_x << ", " << point.m_y << ')';
return out;
}
// Access functions
void setPoint(int x, int y)
{
m_x = x;
m_y = y;
}
};
class Creature
{
private:
std::string m_name;
Point2D m_location;
public:
Creature(const std::string &name, const Point2D &location)
:m_name{name}, m_location{location}
{
}
friend std::ostream& operator<<(std::ostream& out, const Creature &creature)
{
out << creature.m_name << " is at " << creature.m_location;
return out;
}
void moveTo(int x, int y)
{
m_location.setPoint(x, y);
}
};
int main() {
std::cout << "Enter a name for your creature.";
std::string name;
std::cin >> name;
Creature creature{name, {4, 7}};
while (true) {
// print the creature's name and location
std::cout << creature << '\n';
std::cout << "Enter new X location for creature (-1 to quit): ";
int x{0};
std::cin >> x;
if (x == -1) {
break;
}
std::cout << "Enter new Y location for creature (-1 to quit):";
int y{0};
std::cin >> y;
if (y == -1) {
break;
}
creature.moveTo(x, y);
}
return 0;
}
组合的变体
尽管大多数组合模型中,部分随着整体创建而创建,部分随整体销毁而销毁。但是还有一些不一样的情况
例如
- 在组合关系中,可能会延迟某些部分的创建,直到它们需要为止。例如,用户为字符串分配一些要保留的数据之前,字符串类可能不会创建动态字符数组
- 在组合关系中,可能会选择使用已提供给它的零件作为输入,而不是自己创建零件
- 在组合关系中,可能会将其部分的销毁要求委托给其他对象。如,垃圾回收例程
这里的关键点是,组合中整体管理部分,而组合的用户则不需要进行任何管理
组合和子类
当涉及对象组合时新程序员经常会问一个问题:“我什么时候应该使用子类而不是直接实现功能?”
例如,我们可以不使用Point2D类来实现Creature的位置,而可以只向Creature类添加2个整数,并在Creature类中编写代码来处理位置
将Point2D设为自己的类有很多好处:
- 每个对象可以保持简单明了,专注于出色地完成一项任务,由于它们职责更分明,因此使这些类更容易编写和理解
- 例如,Point2D只关注与点有关的时间,这使理解和编写代码更加简单
- 每个子类可以独立的,这使得它们可以重用
- 例如,我们可以在完全不同的应用程序中重用Point2D类
- 或者,我们需要另外一个点作为试图要到达的目的地,那么可以简单添加另一个Point2D成员变量
- 父类可以让子类完成大部分的工作,而自己专注协调子类之间的数据流。这有助于降低父对象的整体复杂性,因为它可以将任务委派给已经知道如何执行这些任务的子对象
- 例如,当我们移动Creature时,它将任务委派给Point类,该类已经了解如何设置点
- 因此,Creature类不必担心如何实现这些事情
一个好的经验法则是,应该构建单个类对应单个任务
在我们的示例中, 很明显Creature不必担心Point的实现方式或名称的存储方式
- Creature的工作不需要知道哪些私密的细节
- Creature的工作是担心如何协调数据流,并确保每个子类都知道应该做什么。接下来有各个子类担心它们怎么做
聚合
概述
要符合聚合条件,整个对象及其各个部件必须具有以下关系
- 该部件(成员)是对象(类)的一部分
- 该部件 (成员) 一次可以属于多个对象(类)
- 该部件 (成员)的存在不由对象(类)管理
- 该部件(成员)不知道对象(类)的存在
像组合一样,聚合仍然是部件与整体的关系,其中部件包含在整体中,并且是单向关系,但是与组合不同,部件一次可以属于一个以上的对象,而整个对象对部件的存在和生命周期不负责
创建聚合时,聚合不负责创建部件,销毁聚合时,聚合不负责销毁部件
例如,考虑一个人与其家庭住址之间的关系
- 在此示例中,为简单起见,我们将说每个人都有一个地址。但是,该地址一次可以属于一个以上的人:如你和你的室友或其他重要的人
- 但是,该地址不是由该人管理的-该地址可能在此人到达之前就已存在,而在该人离开后仍然存在
- 另外,一个人知道他们住的地方,但地址不知道他们住的地方
- 因为这是一个聚合关系
另外,考虑一下汽车和引擎
- 汽车发动机是汽车的一部分,尽管引擎属于汽车,但它也可以属于其他事物,例如拥有汽车的人
- 汽车不负责发动机的制造和毁坏
- 虽然汽车知道它有引擎,但是引擎却不知道它是汽车的一部分
在对物理对象建模时,使用“破坏”一词可能会有些麻烦
- 有人可能会争辩说“如果流星从天上掉下来砸碎了汽车,汽车零件也不会全部被毁吗?”当然是,但这是流星的错
- 重要的是,汽车对零件的损坏不承担任何责任(但可能会受外力的影响)
我们可以说聚合模型“有”关系(一个部分有老师,汽车有引擎)。与组合类似,集合的各个部件可以单独的或相互调用
聚合案例
因为聚合与组合相似,因为它们都是部件 - 整体关系,因此它们的实现几乎完全相同,它们之间的差异主要是语义上的
在组合中,我们通常使用普通成员变量(或有组合类处理分配和释放过程的指针)将零件添加到组合中
在聚合中,我们还将部件添加为成员变量,但是,这些成员变量通常是引用或指针,用于指向已在类范围之外创建的对象。因此,聚合通常要么指向的对象用作构造函数参数,要么开始为空,然后再通过访问函数或运算符添加子对象
因为这些部分存在于类范围之外,所以当销毁该类时,指针或引用成员变量将销毁(但不会删除)。因此,部件本身仍将存在
#include <iostream>
#include <string>
class Teacher
{
private:
std::string m_name{};
public:
Teacher(const std::string& name): m_name{name}
{
}
const std::string& getName() const
{
return m_name;
}
};
class Department
{
private:
const Teacher& m_teacher; // This dept holds only one teacher of simplicity, but it could hold many teachers
public:
Department(const Teacher& teacher): m_teacher{teacher}
{
}
};
int main() {
// Create a teacher outside the scope of the Department
Teacher bob {"Bob"}; // create a teacher
{
// Create a department and use the consturctos partameter to pass
// the teacher to it.
Department department {bob};
} // department goes out of scope here and is destoryed
// bob still exists here, but the department doesn't
std::cout << bob.getName() << " still exists! \n";
return 0;
}
- 首先,该部门只能容纳一位老师。其次,老师不会知道他们所属的部门
- 所以在本例中,bob是独立于department创建的,然后传递给department的构造函数
- 当department 被销毁的时,m_teacher引用被销毁。但是是教师本身并没有被销毁,因此它仍然存在,直到后来在main()被独立销毁
为建模选择正确的关系
尽管上面的例子中教师不知道他们为那么部门工作似乎有些愚蠢,但在给定程序的背景下这可能完全没问题
在确定要实现哪种关系时,请实现最简单的满足你的需求,而不是看起来最适合现实生活的关系
例如,如果你正在编写车身修理厂模拟器,则可能希望将汽车和引擎作为聚合来实现,因此可以将引擎卸下并放在架子上以备后用
但是,如果你正在编写赛车模拟游戏,则可能希望将汽车和引擎作为组合来实现,因为在这种情况下,引擎永远不会存在于汽车外部
关联
概述
要符合关联条件,一个对象与另一个对象必须具有以下关系
- 关联的对象(成员)与对象(类)无关
- 关联的对象(成员)一次可以属于多个对象(类)
- 关联的对象(成员)没有由该成员(类)管理其存在
- 关联的对象(成员)可能知道或可能不知道对象(类)的存在
与组合和聚合的部分是构成整体的一部分不同,在关联中,关联的对象与该对象无关
就像聚合一样,关联的对象可以同时属于多个对象,并且不受这些对象的管理
但是,与聚合(关系始终是单向的)不同的是,在关联中,关系可以是单向的也可以是双向的(两个对象相互了解)
- 医生与患者之间的关系就是一个很好的例子
- 医生显然与患者有关系,但从概念上讲,这个不是部分/整体(对象组合)的关系
- 一个医生一天可以看很多病人,一个病人可以看很多医生(也许想要第二个医生意见,或者他们正在拜访不同类型的医生)
- 该对象的寿命都没有和另外一个有关系
- 我们可以说关联模型的是“用户-单个”关系。医生“使用”病人(以赚取收入)。患者使用医生(出于他们所需的任何健康目的)
案例
#include <cstdint>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
using namespace std;
// Since Doctor and Patient have a circular dependency, we're going to farward declare Patient
class Patient;
class Doctor
{
private:
string m_name{};
vector<reference_wrapper<const Patient>> m_patient{};
public:
Doctor(const string& 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 ostream& operator<<(ostream &out, const Doctor &doctor);
const string& getName() const
{
return m_name;
}
};
class Patient
{
private:
string m_name{};
vector<reference_wrapper<const Doctor>> m_doctor{}; // so that we can use it here
// We're going to make private because we don't want the public to use it
// They should use Doctor::addPatient() instead, which is publiy exposed
void addDoctor(const Doctor& doctor)
{
m_doctor.push_back(doctor);
}
public:
Patient(const string& name): m_name {name}
{
}
// We'll friend Doctor::addPatient() so it can access the private function Patient::addDoctor()
friend ostream& operator << (ostream &out, const Patient &patient);
const 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);
}
ostream& operator<<(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;
}
ostream& operator<<(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 seeding 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);
cout << james << '\n';
cout << scott << '\n';
cout << dave << '\n';
cout << frank << '\n';
cout << betsy << '\n';
return 0;
}
因为关联是一种广泛的关系,所以可以用许多不同的方式来实现它们。但是,大多数情况下,关联是使用指针实现的,其中对象指向关联的对象
在这个示例中,我们将实现双向“医生/患者”关系,因为让医生知道他们的患者是有意义的,反之亦然
通常,如果可以使用单向关联,则应避免使用双向关联,因为它们会增加复杂性,并且往往更容易编写而不会错误
反身关联
有时对象可能与相同类型的其他对象有关系,这被称为反身关联
反身关联的一个很好例子是大学课程与其先决条件(也就是大学课程)之间的关系
考虑简化的情况,其中一门课程只能有一个前提条件。我们可以做这样的是事情
include <string>
class Course
{
private:
std::string m_name;
const Course *m_prerequisite;
public:
Course(const std::string &name, const Course *prerequisite = nullptr): m_name{name}, m_prerequisite{prerequisite}
{
}
};
- 这可能会导致关联链(课程具有先决条件等)
关联可以是间接的
#include <iostream>
#include <string>
using namespace std;
class Car
{
private:
string m_name;
int m_id;
public:
Car(const string& name, int id): m_name{name}, m_id{id}
{
}
const 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 retrieven them
// Because it's static, we don't need to allocate an object of type CarLot to use it
class CarLot
{
private:
static Car s_carLot[4];
public:
CarLot() = delete; // Ensure we don't try to create a CarLot
static Car* getCar(int id)
{
for (int count{0}; count < 4; ++count) {
if (s_carLot[count].getId() == id) {
return &(s_carLot[count]);
}
}
return nullptr;
}
};
Car CarLot::s_carLot[4]{{"Prius", 4}, {"Corolla", 17}, {"Accord", 84}, {"Matrix", 62}};
class Driver
{
private:
string m_name;
int m_carId; // we're associated with the Car by ID rather than pointer
public:
Driver(const string& name, int carId): m_name{name}, m_carId{carId}
{
}
const 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) {
cout << d.getName() << " is driving a " << car->getName() << '\n';
} else {
cout << d.getName() << " couldn't find his car \n";
}
return 0;
}
在上面的示例中,我们有一个CarLot来存放我们的汽车。需要汽车的驾驶员没有指向他的汽车的指针-相反,他具有汽车ID,我们可以使用该ID在需要时从CarLot中获取汽车
在这个特定示例中,以这种方式执行操作有点愚蠢,因为将Car将退出CarLot要求的查找效率很低(将两者连接的指针要快得多)
但是,通过唯一的ID而不是指针来引用事物也具有优势
- 例如,你可以引用当前不在内存中的内容(也许它们在文件或数据中,并且可以按需加载)
- 同样,指针可以占用4或8个字节。如果空间有限,并且唯一对象的数量很少,则用8位或16位整数引用它们可以节省大量内存
总结
组合和聚合的总结
组合
- 通常使用普通成员变量
- 如果类本身处理对象分配/取消,则可以使用指针成语
- 负责部件的创建/销毁
聚合
- 通常使用指向或引用位于聚合类范围之外的对象的指针或引用成员
- 不负责创建/销毁部件
值得注意的是,组合和聚合的概念不是相互排斥的,可以在同一类中自由混合。完全可以编写一个类来负责创建/销毁某些部分,而不是其他
例如,我们的部门类可以有一个名字和一个老师类
- 该名称可以按组合添加到部门中,并随部门一起创建和销毁
- 另一方面,将通过汇总将教师添加到部门,并独立创建/销毁该老师
尽管聚合可能非常有用,但它们也可能更加危险,因为聚合无法处理其部分的重新分配,解除分配留给外部人员完成。如果外部方不再指向废弃部分的指针或引用,或者只是忘记进行清理(假设该类将进行清理),则内存将被泄漏
出于这个原因,应该优先使用组合而不是聚合
例子
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Teacher
{
private:
std::string m_name{};
public:
Teacher(const std::string& name): m_name{name}
{
}
const std::string& getName() const
{
return m_name;
}
};
class Department
{
private:
vector<Teacher> m_listOfTeachers;
public:
Department()
{
}
void add(const Teacher& teacher)
{
m_listOfTeachers.push_back(teacher);
}
friend std::ostream& operator<<(std::ostream& out, const Department &department)
{
out << "Department: ";
for (auto& teac : department.m_listOfTeachers) {
out << teac.getName() << ' ';
}
return out;
}
};
int main() {
// Create a teacher outside the scope of the Department
Teacher t1 {"Bob"};
Teacher t2 {"Frank"};
Teacher t3 {"Beth"};
{
// Create a department and add some Teachers to it
Department department{}; // create an empty Department
department.add(t1);
department.add(t2);
department.add(t3);
cout << department;
} // department goes out of scope here and is destoryed
cout << t1.getName() << " still exists! \n";
cout << t2.getName() << " still exists! \n";
cout << t3.getName() << " still exists! \n";
return 0;
}
组合 vs 聚合 vs 关联 总结
属性 | 组合 | 聚合 | 关联 |
---|---|---|---|
关系类型 | 整体/部分 | 整体/部分 | 没有特定关系 |
成员属于多个类别 | 否 | 是 | 是 |
类管理成员生命周期 | 是 | 否 | 否 |
方向性 | 单向 | 单向 | 单向或双向 |
关系动词 | Part-of | Has-a | Uses-a |