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

这将输出:

image

通常情况下,如果单向关联能够满足需求,就应避免使用双向关联,因为双向关联会增加复杂性,且更容易在编写过程中出现错误。


自反关联

有时对象可能与同类型的其他对象存在关联关系,这称为自反关联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;
}

image

在上例中,我们有一个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
posted @ 2026-01-31 05:22  游翔  阅读(1)  评论(0)    收藏  举报