Association, Aggregation, Composition and Dependency

Learning Objectives

Distinguish association, aggregation, composition and dependency by ownership and lifetime.

Express each relationship in C++ code and in UML notation.

Read multiplicity and navigability.

Model a small domain with the right relationships.

Prerequisites: Modules 2–8.

Intuition and Definition

Typical C++ form A *uses* B temporarily (parameter, local) void print(const Printer& p) dashed arrow ..> A *knows* B (long-term link) non-owning pointer/reference member solid line -->

A *has* B as a part, but B can outlive A non-owning pointer, shared_ptr, IDs hollow diamond o-- A *owns* B; B dies with A B bounded by A member object or unique_ptr filled diamond *--

Multiplicity says how many: 1, 0..1, * (many), 1..*. Navigability says which side knows the other (an arrowhead).

UML practice varies: many teams avoid aggregation entirely because its semantics are vague — when in doubt, state ownership and lifetime in words.

Why It Matters

Ownership decides who deletes what, what happens when an object is removed, and how objects are serialized and tested. Getting it wrong produces leaks, dangling pointers and confusing models.

Syntax and Language Notes

In garbage-collected languages (Java, C#, Python, TypeScript) every member is a reference, so composition is a *design convention*: the whole creates the part, never shares it, and releases it when the whole is discarded.

class Engine {};
class Driver {};
class Garage {};
class Car {
    Engine engine_;                  // composition: created and destroyed with the Car
    Driver* driver_ = nullptr;       // association: Car knows a Driver it does not own
public:
    void parkIn(Garage& g) { (void)g; }   // dependency: used only during the call
};

Complete Example

#include <iostream>
#include <memory>
#include <string>
#include <vector>

struct Part {
    std::string name;
    explicit Part(std::string n) : name(std::move(n)) { std::cout << "  + " << name << "\n"; }
    ~Part() { std::cout << "  - " << name << "\n"; }
};

struct Student {
    std::string name;
    explicit Student(std::string n) : name(std::move(n)) {}
};

class Logger {                       // used only as a dependency
public:
    void write(const std::string& s) const { std::cout << "  log: " << s << "\n"; }
};

class Course {                       // aggregation: students exist independently of the course
    std::string title_;
    std::vector<const Student*> students_;
public:
    explicit Course(std::string t) : title_(std::move(t)) {}
    void enrol(const Student& s) { students_.push_back(&s); }
    void report(const Logger& log) const {          // dependency on Logger
        log.write(title_ + " has " + std::to_string(students_.size()) + " students");
    }
};

class Car {                          // composition: the car owns its engine and wheels
    Part engine_{"engine"};
    std::vector<std::unique_ptr<Part>> wheels_;
public:
    Car() { for (int i = 1; i <= 2
…
composition:
  + engine
  + wheel 1
  + wheel 2
  (car in use)
  - wheel 2
  - wheel 1
  - engine
aggregation + dependency:
  log: OOP has 2 students
  students still alive: Ana, Raj

Step-by-Step Walkthrough

Creating the Car creates its parts; leaving the scope destroys the car and every part (members in reverse order: wheels vector, then engine).

The Course stores non-owning pointers to students. Destroying the course does not destroy students — aggregation.

report receives a Logger only for the duration of the call — a dependency.

classDiagram
  Car *-- "1" Engine
  Car *-- "2..*" Wheel
  Course o-- "0..*" Student
  Course ..> Logger : uses
  Driver "1" --> "0..1" Car : drives

Common Mistakes

Raw owning pointers (Part* p = new Part) for composition — use members or unique_ptr.

Aggregation via non-owning pointers to objects that die first — dangling pointers. Document lifetimes or use IDs/weak_ptr.

Bidirectional links everywhere (Course ↔ Student), which must be kept in sync and create cycles.

Modelling every association as inheritance ("Student extends Person extends Course…").

Trade-offs and Alternatives

Identifiers (StudentId) instead of pointers make aggregation robust across persistence and threads, at the cost of a lookup.

shared_ptr expresses shared ownership but hides who really owns what and can form cycles (break with weak_ptr, Module 13).

Real-World Use

Modelling exercises — choose the relationship:

University — Department composition (departments don't exist without the university) Department — Professor aggregation/association (professors can move) Order — OrderLine Order — Customer association (by customer id) ParkingLot — ParkingSpot Ticket — Vehicle ECU — CAN driver composition of the driver instance, dependency on the bus abstraction

Practice

Q1. (Easy) A function takes a Printer& parameter only to print. Which relationship?

Answer: Dependency.

Q2. (Medium) Give the UML symbol and C++ form for composition.

Answer: A filled diamond on the whole's side; in C++ a member object or std::unique_ptr member.

Q3. (Advanced) A playlist holds songs that also belong to a library. Removing the playlist must not delete songs; deleting a song from the library must remove it from all playlists. Propose a design.

Answer: The Library owns songs (composition, e.g. std::map<SongId, Song>). Playlists aggregate songs by SongId. When a song is deleted, the library publishes a "song removed" event (Observer, Module 41) and playlists drop that id — or playlists resolve ids lazily and skip missing ones. No playlist holds a pointer that could dangle.

Interview Question

Q1. "Explain aggregation vs composition with an example and say how you'd implement each in C++."

Answer: Composition: a House and its Rooms — rooms are created with the house and destroyed with it; implement as member objects or std::unique_ptr<Room> members. Aggregation: a Team and its Players — players exist before and after the team; implement as non-owning references/pointers or IDs, with an explicit statement of who owns the players (e.g. a League registry). The deciding questions are *who creates it, who destroys it, and can it be shared?*

Quick Quiz

Q1. Which relationship ties the part's lifetime to the whole?

Answer: Composition.

Q2. What does 0..* mean?

Answer: Zero or more.

Q3. Which UML arrow is dashed with an open head?

Answer: Dependency.

Summary

Dependency (uses), association (knows), aggregation (has, independent lifetime), composition (owns, shared lifetime).

Ownership and lifetime are the deciding questions; express them clearly in code.

Related: Module 13 (smart pointers), Module 23 (UML class diagrams), Module 74 (DDD aggregates).