Skip to content

Latest commit

 

History

History
328 lines (232 loc) · 13.6 KB

16.4 — Association.md

File metadata and controls

328 lines (232 loc) · 13.6 KB

16.4 — Association

In the previous two lessons, we’ve looked at two types of object composition, composition and aggregation. Object composition is used to model relationships where a complex object is built from one or more simpler objects (parts).

在前面的两节课中我们已经学习了组合和聚合。组合是用简单对象来构建复杂对象的过程。

In this lesson, we’ll take a look at a weaker type of relationship between two otherwise unrelated objects, called an association. Unlike object composition relationships, in an association, there is no implied whole/part relationship.

在这一节我们要观察一种较弱的关系类型。它发生在两个本不相干的对象之间,叫做结合。在结合中没有部分和整体的关系。

Association

To qualify as an association, an object and another object must have the following relationship:

验证一个结合关系,一个对象和另一个对象必须拥有下面的几个关系

  • The associated object (member) is otherwise unrelated to the object (class)

    被结合的对象和另一个对象本来没有关系

  • The associated object (member) can belong to more than one object (class) at a time

    被结合的对象可以属于多个对象,在同一时间内。

  • The associated object (member) does not have its existence managed by the object (class)

    被结合的对象的生命周期不由另一个对象来管理。

  • The associated object (member) may or may not know about the existence of the object (class)

    被结合的对象可能知道也可能不知道另一个对象的存在

Unlike a composition or aggregation, where the part is a part of the whole object, in an association, the associated object is otherwise unrelated to the object. Just like an aggregation, the associated object can belong to multiple objects simultaneously, and isn’t managed by those objects. However, unlike an aggregation, where the relationship is always unidirectional, in an association, the relationship may be unidirectional or bidirectional (where the two objects are aware of each other).

跟组合和聚合不一样,在结合中,被结合的对象和另一个对象本来没有关系。被结合的对象可以同时属于多个对象,在聚合中这个关系通常是单向的,而在结合中,这个关系可能是单向的也可能是双向的。

The relationship between doctors and patients is a great example of an association. The doctor clearly has a relationship with his patients, but conceptually it’s not a part/whole (object composition) relationship. A doctor can see many patients in a day, and a patient can see many doctors (perhaps they want a second opinion, or they are visiting different types of doctors). Neither of the object’s lifespans are tied to the other.

医生和病人之间的关系是一个很好的例子。医生显然和他的病人有关系。一个医生可以有很多的病人,一个病人也可以看很多的医生。谁的生命周期也不绑在另一个人的身上。

We can say that association models as “uses-a” relationship. The doctor “uses” the patient (to earn income). The patient uses the doctor (for whatever health purposes they need).

我们用“uses-a” 来描述结合关系。医生用病人来赚钱,而病人用医生来治病。

Implementing associations

Because associations are a broad type of relationship, they can be implemented in many different ways. However, most often, associations are implemented using pointers, where the object points at the associated object.

因为结合是一种宽松的关系,所以它可以有很多的实现方式。然而最常见的结合方式是使用指针,对象指向被结合的对象(就像你请别人帮忙,至少得知道别人的手机号)。

In this example, we’ll implement a bi-directional Doctor/Patient relationship, since it makes sense for the Doctors to know who their Patients are, and vice-versa.

在这个例子中我们会实现一个双向的医生病人关系。所以它实现了病人知道医生,医生知道病人这样的现实意义。

#include <cstdint>
#include <functional> // reference_wrapper
#include <iostream>
#include <string>
#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(const std::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 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(const std::string& name)
		: m_name{ name }
	{
	}
 
	// We'll implement this function below Doctor since we need Doctor to be defined at that point
	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;
}

This prints:

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

In general, you should avoid bidirectional associations if a unidirectional one will do, as they add complexity and tend to be harder to write without making errors.

通常,你应该避免双向关系,如果你可以用单向关系搞定事情的话。因为双向关系会比单向关系复杂,而且容易在编写的时候写出错误。

Reflexive association

Sometimes objects may have a relationship with other objects of the same type. This is called a reflexive association. A good example of a reflexive association is the relationship between a university course and its prerequisites (which are also university courses).

有时候一个对象可能和同类型的另一个对象拥有联系。这叫做reflexive 结合关系。一个好的例子是大学课程和它的前置课程之间的关系。

Consider the simplified case where a Course can only have one prerequisite. We can do something like this:

思考下面的只有一个前置课程的例子。

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

This can lead to a chain of associations (a course has a prerequisite, which has a prerequisite, etc…)

这会导致一个前置的链的形式出现。

Associations can be indirect

结合也可以是间接的

In all of the previous cases, we’ve used either pointers or references to directly link objects together. However, in an association, this is not strictly required. Any kind of data that allows you to link two objects together suffices. In the following example, we show how a Driver class can have a unidirectional association with a Car without actually including a Car pointer or reference member:

在前面所有的例子中,我们已经使用了指针或者引用来间接的指向对象。然而在结合中,这件事情不是严格要求的。任何类型的数据,只要允许你连接两个对象,都是可以的。在下面的例子中,我们会展示一种司机类型,和汽车类型有一个单向的联系而不使用指针或者引用。

#include <iostream>
#include <string>
 
class Car
{
private:
	std::string m_name;
	int m_id;
 
public:
	Car(const std::string& 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
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:
	std::string m_name;
	int m_carId; // we're associated with the Car by ID rather than pointer
 
public:
	Driver(const std::string& 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;
}

In the above example, we have a CarLot holding our cars. The Driver, who needs a car, doesn’t have a pointer to his Car -- instead, he has the ID of the car, which we can use to get the Car from the CarLot when we need it.

在上面的例子中,我们用车库来存车。司机需要一辆车但是并不是直接指向他的车,而是他拥有车的ID,在需要用车的时候通过ID从车库中提出。

In this particular example, doing things this way is kind of silly, since getting the Car out of the CarLot requires an inefficient lookup (a pointer connecting the two is much faster). However, there are advantages to referencing things by a unique ID instead of a pointer. For example, you can reference things that are not currently in memory (maybe they’re in a file, or in a database, and can be loaded on demand). Also, pointers can take 4 or 8 bytes -- if space is at a premium and the number of unique objects is fairly low, referencing them by an 8-bit or 16-bit integer can save lots of memory.

在这个特定的例子中,这样做看起来有点傻。因为从车库中提车需要一个效率不高的循环。然而,这样做也有好处。举个例子,你可以指向一个现在不在内存中的对象。另外,指针要占用4到8个字节,如果程序使用的空间是优先考虑的,而且要使用的对象的数量也比较少的话,用8位的int可能更节省空间。

Composition vs aggregation vs association summary

Here’s a summary table to help you remember the difference between composition, aggregation, and association:

下面是一个总结,从关系的类型,成员能不能属于多个对象,成员的生命周期是否由关系的另一方管理。这种关系是单向的还是双向的。关系动词是什么五个方面来考虑。

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