23.4 — 关联

在前面的两节课中,我们学习了两种类型的对象组合:复合和聚合。对象组合用于建模复杂对象由一个或多个简单对象(部分)构建的关系。

在本节课中,我们将探讨两种原本不相关的对象之间的一种较弱的关系,称为关联。与对象组合关系不同,在关联中,不存在隐含的整体/部分关系。

关联

要符合关联的条件,一个对象和另一个对象必须具有以下关系:

  • 关联对象(成员)与对象(类)原本不相关
  • 关联对象(成员)可以同时属于多个对象(类)
  • 关联对象(成员)的生命周期不受对象(类)管理
  • 关联对象(成员)可能知道也可能不知道对象(类)的存在

与复合或聚合不同,在复合或聚合中,部分是整体对象的一部分,而在关联中,关联对象与对象原本不相关。就像聚合一样,关联对象可以同时属于多个对象,并且不受这些对象的管理。然而,与聚合不同的是,聚合中的关系总是单向的,而在关联中,关系可以是单向的,也可以是双向的(两个对象彼此都知晓)。

医生和患者之间的关系是关联的一个很好的例子。医生与他的患者显然有关系,但从概念上讲,它不是部分/整体(对象组合)关系。医生一天可以看很多患者,患者也可以看很多医生(也许他们想要第二种意见,或者他们正在看不同类型的医生)。这两个对象的生命周期互不关联。

我们可以说关联建模为“使用(uses-a)”关系。医生“使用”患者(以赚取收入)。患者使用医生(出于他们需要的任何健康目的)。

实现关联

由于关联是一种广泛的关系类型,因此可以通过多种不同的方式实现。然而,通常情况下,关联是使用指针实现的,其中对象指向关联对象。

在这个例子中,我们将实现一个双向的医生/患者关系,因为医生了解他们的患者以及反之亦然是很合理的。

#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;
}

这会打印

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

一般来说,如果单向关联可以满足需求,则应避免双向关联,因为它们会增加复杂性,并且更容易出错。

自关联

有时对象可能与相同类型的其他对象存在关系。这称为自关联。自关联的一个很好的例子是大学课程及其先决条件(也都是大学课程)之间的关系。

考虑简化情况,即一门课程只能有一个先决条件。我们可以这样做:

#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 没有指向其 Car 的指针——相反,他拥有汽车的 ID,我们可以使用它在需要时从 CarLot 中获取 Car。

在这个特定的例子中,这样做有点傻,因为从 CarLot 中取出 Car 需要低效的查找(连接两者的指针要快得多)。然而,通过唯一 ID 而不是指针引用事物有其优点。例如,您可以引用当前不在内存中的事物(它们可能在文件中,或在数据库中,可以按需加载)。此外,指针可能占用 4 或 8 字节——如果空间非常宝贵且唯一对象的数量相当少,通过 8 位或 16 位整数引用它们可以节省大量内存。

复合 vs 聚合 vs 关联 总结

下面是一个总结表,帮助您记住复合、聚合和关联之间的区别:

属性组合聚合关联
关系类型整体/部分整体/部分原本不相关
成员可以属于多个类
成员的生命周期由类管理
方向性单向单向单向或双向
关系动词是…的一部分拥有使用
guest
您的电子邮箱地址将不会被显示
发现错误?请在上方留言!
与勘误相关的评论在处理后将被删除,以帮助减少混乱。感谢您帮助使网站对每个人都更好!
来自 https://gravatar.com/ 的头像与您提供的电子邮箱地址相关联。
有回复时通知我:  
199 评论
最新
最早 最多投票
内联反馈
查看所有评论