Version sans JavaScript, en lecture seule : les ateliers, les quiz et l’exécution du code n’y sont pas. Version complète · Catalogue · Se connecter

Support de cours · chapitre 3

La lutte des classes

Les quatre relations entre classes (association, agrégation, composition, héritage) et le critère qui permet de les distinguer, la durée de vie des objets.

Ce chapitre prolonge la séquence 1. Il présente les quatre relations entre classes, leur notation UML, et une erreur de lecture fréquente qui les fait paraître identiques dans le code.

Association : un simple lien

Dans une association, deux objets se connaissent, mais aucun ne dépend de l'autre pour exister.

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

class Dresseur {
    private Canard canard;
    Dresseur(Canard c) { this.canard = c; }   // on range la référence
}

En UML, l'association se note par un trait simple. Le dresseur entraîne le canard, mais si l'on supprime l'un des deux, l'autre continue d'exister.

Ici, ranger la référence telle quelle est correct, puisque c'est le sens même de la relation : le dresseur doit s'adresser à ce canard précis, et non à une copie.

Variante bidirectionnelle. Il arrive que le canard doive aussi connaître son dresseur. C'est possible, mais cela ajoute du couplage : les deux classes ne peuvent plus être lues l'une sans l'autre. On ne le fait donc que si le besoin l'impose.

Agrégation : « possède », faiblement

Dans une agrégation, un objet en regroupe d'autres, qui peuvent continuer d'exister sans lui.

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

En UML, l'agrégation se note par un losange blanc. La troupe est une collection de références : si on la dissout, les canards continuent d'exister.

Composition : « fait partie de »

Dans une composition, la partie est créée et détruite avec le tout.

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

En UML, la composition se note par un losange noir. Les ailes n'existent pas sans le canard : c'est lui qui les crée, et aucun autre objet n'en garde de référence.

C'est ce dernier point qui caractérise la composition, et c'est aussi celui qu'on oublie le plus souvent. Un constructeur qui reçoit l'aile de l'extérieur et la range telle quelle ne réalise pas une composition, car l'appelant garde une référence vers l'objet.

class Canard {
    private final Aile gauche;
    Canard(Aile gauche) {
        this.gauche = gauche;          // ← l'appelant tient encore l'aile
    }
}

Le mot-clé final n'y change rien, car il rend la référence constante mais pas l'objet. La démonstration se trouve au chapitre 2 ; il est utile de l'exécuter au moins une fois.

Pour obtenir une vraie composition, il faut faire une copie :

Canard(Aile modele) {
    this.gauche = new Aile(modele.puissance());   // un exemplaire à soi
}

Héritage : « est un »

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 !"); }
}

En UML, l'héritage se note par une flèche à pointe creuse dirigée vers la super-classe. La sous-classe hérite du comportement de la super-classe et peut le spécialiser. C'est la relation la plus puissante, mais aussi la plus coûteuse ; le chapitre 4 lui est entièrement consacré.

Récapitulatif UML

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

    Dresseur --> Canard : association
    Troupe o-- Canard : agrégation
    Canard *-- Aile : composition
    Canard <|-- CanardColvert : héritage
    Canard <|-- CanardEnPlastique : héritage

Les classes fourre-tout

Une classe à laquelle on ajoute sans cesse des méthodes finit par tout faire, et ses relations avec les autres classes deviennent difficiles à lire.

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

    void voler() { }
    void nager() { }
    void chanter() { }
    void sauvegarderEnXML() { }      // ← rien à voir
    void imprimerFacture() { }       // ← rien à voir
    void gererStock() { }            // ← rien à voir
}

On appelle ce type de classe un God Object. Il est traité au chapitre 8 ; retenez pour l'instant qu'on corrige ce défaut par la composition :

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

Chaque responsabilité est placée dans sa propre classe, et le canard combine ces objets au lieu de contenir lui-même tout le code.

Question d'épreuve

Qu'est-ce qui distingue une composition d'une agrégation dans le code ?

La composition crée elle-même la partie et n'en laisse aucune référence à l'extérieur ; l'agrégation reçoit des éléments qui continuent d'exister sans elle. Dans le constructeur, on trouve donc un new dans le premier cas et une simple affectation dans le second.