Version without JavaScript, read-only: workshops, quizzes and running code are not available here. Full version · Catalogue · Sign in

Lecture notes · chapter 3

The class struggle

The four relationships between classes (association, aggregation, composition, inheritance) and the criterion that tells them apart, the lifetime of the objects.

This chapter extends sequence 1. It presents the four relationships between classes, their UML notation, and a common misreading that makes them look identical in the code.

Association: a simple link

In an association, two objects know each other, but neither depends on the other to exist.

class Canard {
    private String nom;
    Canard(String nom) { this.nom = nom; }
}

class Dresseur {
    private Canard canard;
    Dresseur(Canard c) { this.canard = c; }   // we store the reference
}

In UML, an association is drawn as a plain line. The trainer trains the duck, but if either one is deleted, the other still exists.

Here, storing the reference as is is correct, since that is what the relationship means: the trainer must deal with that particular duck, not with a copy.

Bidirectional variant. Sometimes the duck also needs to know its trainer. This is possible, but it adds coupling: the two classes can no longer be read independently. It should therefore only be done when the need requires it.

Aggregation: “has”, weakly

In an aggregation, an object groups others, which can go on existing without it.

class Troupe {
    private List<Canard> canards = new ArrayList<>();
    void ajouter(Canard c) { canards.add(c); }
}

In UML, an aggregation is drawn with a white diamond. The troupe is a collection of references: if it is disbanded, the ducks go on existing.

Composition: “is part of”

In a composition, the part is created and destroyed along with the whole.

class Aile {
    private int puissance;
    Aile(int puissance) { this.puissance = puissance; }
}

class Canard {
    private Aile aileGauche = new Aile(10);
    private Aile aileDroite = new Aile(10);
}

In UML, a composition is drawn with a black diamond. The wings do not exist without the duck: the duck creates them, and no other object keeps a reference to them.

This last point is what characterises a composition, and it is also the one most often overlooked. A constructor that receives the wing from outside and stores it as is does not produce a composition, because the caller still holds a reference to the object.

class Canard {
    private final Aile gauche;
    Canard(Aile gauche) {
        this.gauche = gauche;          // ← the caller still holds the wing
    }
}

The final keyword makes no difference, because it makes the reference constant but not the object. The demonstration is in chapter 2; it is useful to run it at least once.

To obtain a real composition, you have to make a copy:

Canard(Aile modele) {
    this.gauche = new Aile(modele.puissance());   // a copy of its own
}

Inheritance: “is a”

class Canard {
    void cancaner() { System.out.println("Coin coin !"); }
}

class CanardColvert extends Canard {
    @Override void cancaner() { System.out.println("Coin coin (colvert) !"); }
}

class CanardEnPlastique extends Canard {
    @Override void cancaner() { System.out.println("Couic couic !"); }
}

In UML, inheritance is drawn as an arrow with a hollow head pointing to the superclass. The subclass inherits the behaviour of the superclass and can specialise it. It is the most powerful relationship, but also the most costly; chapter 4 is entirely devoted to it.

UML summary

classDiagram
    direction LR
    class Dresseur
    class Troupe
    class Canard
    class Aile
    class CanardColvert
    class CanardEnPlastique

    Dresseur --> Canard : association
    Troupe o-- Canard : aggregation
    Canard *-- Aile : composition
    Canard <|-- CanardColvert : inheritance
    Canard <|-- CanardEnPlastique : inheritance

Catch-all classes

A class to which methods keep being added ends up doing everything, and its relationships with other classes become hard to read.

class Canard {
    private String nom;
    private int vitesse;
    private String couleur;

    void voler() { }
    void nager() { }
    void chanter() { }
    void sauvegarderEnXML() { }      // ← nothing to do with it
    void imprimerFacture() { }       // ← nothing to do with it
    void gererStock() { }            // ← nothing to do with it
}

This kind of class is called a God Object. It is dealt with in chapter 8; for now, remember that the defect is corrected through composition:

class Canard {
    private String nom;
    private VolStrategy vol;
    private ChantStrategy chant;
}

Each responsibility is placed in its own class, and the duck combines these objects instead of containing all the code itself.

Exam question

What distinguishes a composition from an aggregation in the code?

A composition creates the part itself and leaves no reference to it outside; an aggregation receives elements that go on existing without it. In the constructor, you therefore find a new in the first case and a plain assignment in the second.