Search

10.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).

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.

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).

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

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 above cases, we’ve used a pointer 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 member:

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.

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.

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
10.5 -- Dependencies
Index
10.3 -- Aggregation

96 comments to 10.4 — Association

  • Louis Cloete

    Hi @Alex. I had a hunch that rearranging the two classes will enable you to friend only void Doctor::addPatient(Patient *pat) instead of the whole class Doctor. Here is my adjusted code:

    Now my question is, is there any benefit doing things the one way or another? I would guess that my adjusted code is better, since you expose less of the private innards of class Patient to the outside via Doctor's public interface. It is also expressing intent more clearly and directly in code. Am I reasoning correctly?

    • Alex

      For this particular example, your solution is better because it exposes less. If Patient and Doctor were more intertwined, having to friend each individual function might be burdensome -- but that's not the case here. I've updated the lesson incorporating your adjustment. Thanks!

  • lucieon

    Hello.

    I tried to code the Doctor-Patient program using header files but it doesn't compile.
    My code:

    1) Doctor.h

    2) Patient.h
    Note: Here I made the addDoctor(Doctor *) function public because I don't know how to friend a Patient class' function in Doctor class.

    3) Doctor.cpp

    4) Patient.cpp

    5) association.cpp

    And the output I get after building the project:

    Is it possible to do resolve the circular dependency between Doctor and Patient this way? If not then can you please explain why?
    And is there any other alternative than the forward declaration option because it just feels unnatural to code it like that.

    Thank you!

    • Hi Lucieon!

      > Is it possible to do resolve the circular dependency between Doctor and Patient this way? If not then can you please explain why?
      No, now you have circular includes.

      > is there any other alternative than the forward declaration
      No, you need forward declarations. Use as few includes as possible in header files.

    • Louis Cloete

      You need to forward declare class Doctor; in Patient.h and class Patient; in Doctor.h and then #include both Patient.h and Doctor.h in both of Patient.cpp and Doctor.cpp. You should also declare void Doctor::addPatient(Patient *pat) as a friend of class Patient. Then your code should be equivalent to the example given in the text.

  • beginnerCoder

    Hello! I wonder why we don't also add this line of code to the function addDoctor:

    doc->addPatient(this)

    since if a patient sees a doctor then the doctor has a new patient!

  • Gaurav Arya

    Hi Alex,

    Taking the example of university course and its prerequisite course, will the relationship between them still be called reflexive if there are multiple prerequisites to a single course? A trivial example would be physics, for which one may have 2 prerequisites maths and english.

  • So linked list is implemented using reflexive association relation...

  • Hi Alex,

    I see that for functions taking in std::string as an argument, you sometimes pass by value and at other times pass by reference (even though it isn't being modified.) I'd really like to know what the best practice is.

    If the modification is to be seen in the calling function, then yes, pass by reference is the way to go. If not, what's the best practice? Pass by value, or a (const) reference?

    Thanks,
    Nitin

    • Hi Nitin!

      If the argument isn't being modified, pass by const reference, unless it's a built-in type (int, float, double, ...).
      If the argument is being modified, pass by pointer if the caller is supposed to see the change, otherwise pass by value.
      Only pass by non-const reference, when passing by pointer is not possible or too complicated (eg. operators).

  • David

    Hi Alex! I wanted to implement the Patient/Doctor classes with each class' member functions defined in separate cpp files. Is the following code the best way to do this:

    patient.hpp:

    patient.cpp:

    doctor.hpp:

    doctor.cpp:

    • Hi David!

      * @Patient::getName and @Doctor::getName should be defined in source files and should return const references.
      * @Patient::Patient and @Doctor::Doctor should be defined in source files.
      * @Patient doesn't ever edit it's doctors, you could store const doctors.
      * @patient.hpp:20, @patient.cpp:10,18, @doctor.hpp:18, @doctor.cpp:12,20: Use uniform initizalization.
      * The name printing loops could be for-each loops, because you don't need to index.
      * @std::vector::size returns a @std::size_t, use that instead of unsigned long.
      * Both operator<<s: Use @std::vector::empty.

      • David

        Hi nascardriver! Thanks for all the great feedback. One question - if I define @Patient::Patient and @Doctor::Doctor in source files, how will the definitions "see" one another? Would I need to #include the source files?

Leave a Comment

Put all code inside code tags: [code]your code here[/code]