Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 526 minIntermédiaire

Associations et composition : du diagramme de classes au code

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • lire les multiplicités et la navigabilité d'un diagramme de classes ;
  • traduire une association 1, 0..1 ou * en C# ;
  • garder cohérente une association bidirectionnelle ;
  • distinguer agrégation et composition dans le code ;
  • dessiner le diagramme de classes d'un code existant.

4 exercices corrigés1 schéma

Une association est un lien durable entre deux classes : un employé appartient à un service, un ticket concerne un poste. En C#, elle se traduit par une propriété qui désigne l'autre objet, ou par une collection quand il peut y en avoir plusieurs. Ce chapitre apprend à lire les associations d'un diagramme de classes UML (multiplicités, navigabilité, agrégation, composition, classe d'association) et à les coder, dans les deux sens : du diagramme au code et du code au diagramme.

Les relations entre classes

Les objets d'une application ne vivent pas seuls : ils se connaissent et s'appellent. UML distingue plusieurs types de relations, du plus faible au plus fort.

RelationSensNotation UMLExemple GestParc
Dépendanceune classe en utilise une autre ponctuellement, sans la mémoriserflèche en pointillésRapport.Generer(List<Ticket>) reçoit des tickets en paramètre
Associationune classe connaît durablement une autretrait plein, multiplicités aux extrémitésun poste est affecté à un employé
Agrégationassociation « tout et parties », les parties vivent sans le toutlosange vide côté toutun service regroupe des employés
Compositionagrégation forte : les parties naissent et meurent avec le toutlosange plein côté toutun ticket contient ses interventions
Héritage (généralisation)une classe est une sorte de une autreflèche à triangle videun portable est un poste (chapitre 7)

Le diagramme de classes ci-dessous résume le domaine de GestParc. Le reste du chapitre le traduit en C#, association par association. La notation UML complète (visibilités, stéréotypes, autres diagrammes) est détaillée dans le cours UML, en particulier le chapitre Héritage, agrégation et composition.

ServicecodelibelleEmployenomprenomNomCompletPostenumInventaireInstallationdateLogicielnomnbLicencesTicketnumerotitreDureeTotaleMinInterventiondatedureeMincompteRendu10..*responsable0..10..10..*1demandeur0..*1technicien0..*10..*0..*0..*
Le domaine de GestParc en UML : associations, agrégation, composition et classe d'association

Lire un diagramme de classes

Chaque classe est un rectangle à trois compartiments : le nom, les attributs (les champs ou propriétés), les opérations (les méthodes). Devant chaque membre, un signe donne la visibilité :

Signe UMLVisibilitéC#
+publiquepublic
-privéeprivate
#protégéeprotected
~paquetageinternal

Les attributs s'écrivent - nom : string, les opérations + Resoudre(date : DateTime) : void. Un membre souligné est statique ; un nom de classe ou d'opération en italique est abstrait.

Aux extrémités de chaque association, la multiplicité dit combien d'objets de cette classe sont liés à un objet de l'autre classe :

MultiplicitéLectureTraduction en C#
1exactement unune propriété non nullable : Service Service
0..1zéro ou unune propriété nullable : Employe? Utilisateur
* ou 0..*zéro, un ou plusieursune collection : List<Intervention> exposée en IReadOnlyList<Intervention>
1..*au moins unune collection, et un constructeur qui exige le premier élément
2..5entre deux et cinqune collection, avec un contrôle du nombre dans les méthodes d'ajout

La navigabilité, une flèche au bout du trait, indique quelle classe connaît l'autre. Sans flèche, l'association est navigable dans les deux sens, ou le sujet n'a pas tranché : traduis seulement les sens dont le code a besoin. Le rôle, écrit près d'une extrémité (demandeur, technicien), donne le nom de la propriété.

Traduire une multiplicité 1 ou 0..1

Une association vers un seul objet devient une propriété du type de l'autre classe. La multiplicité décide de la nullabilité.

csharp
class Poste
{
    public Poste(string numInventaire) => NumInventaire = numInventaire;

    public string NumInventaire { get; }
    public Employe? Utilisateur { get; private set; }     // 0..1 : null quand le poste est en stock

    public void AffecterA(Employe e) => Utilisateur = e;
    public void Liberer() => Utilisateur = null;
}
csharp
Poste pc = new Poste("PC-0007");
pc.AffecterA(julie);
Console.WriteLine($"{pc.NumInventaire} utilisé par {pc.Utilisateur?.NomComplet}");
pc.Liberer();
Console.WriteLine($"{pc.NumInventaire} : {(pc.Utilisateur is null ? "en stock" : "affecté")}");
text
PC-0007 utilisé par Julie Lefèvre
PC-0007 : en stock

Une multiplicité 1 se traduit par une propriété non nullable que le constructeur exige : un ticket a toujours un demandeur, donc Ticket(int numero, string titre, Employe demandeur). Le compilateur, avec les types nullables activés, t'avertit si tu oublies de l'initialiser.

Traduire une multiplicité *

Une association vers plusieurs objets devient une collection privée, exposée en lecture seule, avec des méthodes pour la faire évoluer (le chapitre 3 explique pourquoi).

csharp
class Service
{
    private readonly List<Employe> employes = new List<Employe>();

    public Service(string code, string libelle)
    {
        Code = code;
        Libelle = libelle;
    }

    public string Code { get; }
    public string Libelle { get; }
    public IReadOnlyList<Employe> Employes => employes;

    // internal : seule la classe Employe (même projet) s'en sert, pour garder les deux côtés cohérents
    internal void Ajouter(Employe e) => employes.Add(e);
    internal void Retirer(Employe e) => employes.Remove(e);
}

Les sujets nomment souvent ces collections avec le préfixe les : lesEmployes, lesInterventions. En C#, le nom au pluriel suffit.

Association bidirectionnelle : garder les deux côtés cohérents

Entre Service et Employe, le diagramme est navigable dans les deux sens : un employé connaît son service, un service connaît ses employés. Le code doit alors garantir que les deux côtés disent la même chose. Si Julie change de service, il faut la retirer de l'ancien, l'ajouter au nouveau et mettre à jour sa propriété, en une seule opération.

csharp
class Employe
{
    public Employe(string nom, string prenom, Service service, Employe? responsable = null)
    {
        Nom = nom;
        Prenom = prenom;
        Service = service;
        Responsable = responsable;
        service.Ajouter(this);                            // l'autre côté du lien
    }

    public string Nom { get; }
    public string Prenom { get; }
    public Service Service { get; private set; }          // multiplicité 1
    public Employe? Responsable { get; set; }             // association réflexive, 0..1
    public string NomComplet => $"{Prenom} {Nom}";

    public void ChangerDeService(Service nouveau)
    {
        Service.Retirer(this);
        nouveau.Ajouter(this);
        Service = nouveau;
    }
}
csharp
Service info = new Service("INFO", "Informatique");
Service compta = new Service("COMPTA", "Comptabilité");

Employe antoine = new Employe("Mercier", "Antoine", info);
Employe chloe = new Employe("Lambert", "Chloé", info, antoine);
Employe julie = new Employe("Lefèvre", "Julie", compta);

Console.WriteLine($"{info.Libelle} : {info.Employes.Count} employés");
Console.WriteLine($"Responsable de Chloé : {chloe.Responsable?.NomComplet ?? "aucun"}");

julie.ChangerDeService(info);
Console.WriteLine($"{compta.Libelle} : {compta.Employes.Count}, {info.Libelle} : {info.Employes.Count}");
text
Informatique : 2 employés
Responsable de Chloé : Antoine Mercier
Comptabilité : 0, Informatique : 3

Les méthodes Ajouter et Retirer de Service sont internal : elles ne font pas partie de l'interface publique, seul Employe les appelle. Un code extérieur ne peut donc pas ajouter un employé à un service sans changer la propriété Service de l'employé.

Association réflexive

Une association réflexive relie une classe à elle-même : un employé a pour responsable un autre employé. Elle se code comme une autre, avec une propriété du type de la classe : public Employe? Responsable { get; set; }. La multiplicité 0..1 vient de la directrice, qui n'a pas de responsable ; c'est la colonne id_responsable qui accepte NULL dans la base parc du cours SQL.

Agrégation et composition

L'agrégation et la composition sont des associations « tout et parties ». La différence est le cycle de vie des parties.

Agrégation (losange vide)Composition (losange plein)
Les parties existent sans le tout ?ouinon
Une partie peut appartenir à plusieurs touts ?ouinon, à un seul
Qui crée les parties ?le code extérieur, qui les passe au toutle tout lui-même
Supprimer le tout supprime les parties ?nonoui
Exempleun service et ses employés : supprimer le service ne licencie personneun ticket et ses interventions : une intervention sans ticket n'a pas de sens

En C#, la composition se reconnaît à ce que le tout crée ses parties et ne les laisse pas fabriquer ailleurs :

csharp
class Ticket
{
    private readonly List<Intervention> interventions = new List<Intervention>();

    public Ticket(int numero, string titre, Employe demandeur)
    {
        Numero = numero;
        Titre = titre;
        Demandeur = demandeur;
    }

    public int Numero { get; }
    public string Titre { get; }
    public Employe Demandeur { get; }
    public IReadOnlyList<Intervention> Interventions => interventions;
    public int DureeTotaleMin => interventions.Sum(i => i.DureeMin);

    // Composition : le ticket crée lui-même ses interventions
    public void AjouterIntervention(Employe technicien, DateTime date, int dureeMin, string compteRendu)
    {
        interventions.Add(new Intervention(technicien, date, dureeMin, compteRendu));
    }
}
csharp
Ticket ticket = new Ticket(1, "Écran noir", julie);
ticket.AjouterIntervention(chloe, new DateTime(2026, 11, 2, 10, 0, 0), 30, "Câble HDMI remplacé");
ticket.AjouterIntervention(chloe, new DateTime(2026, 11, 2, 14, 0, 0), 15, "Contrôle avec l'utilisatrice");
Console.WriteLine($"Ticket {ticket.Numero} : {ticket.Interventions.Count} interventions, {ticket.DureeTotaleMin} min");
foreach (Intervention i in ticket.Interventions)
{
    Console.WriteLine($"  {i.Date:HH:mm} {i.Technicien.Prenom} {i.DureeMin} min – {i.CompteRendu}");
}
text
Ticket 1 : 2 interventions, 45 min
  10:00 Chloé 30 min – Câble HDMI remplacé
  14:00 Chloé 15 min – Contrôle avec l'utilisatrice

Pour aller jusqu'au bout, le constructeur d'Intervention peut être internal : seul le projet qui contient Ticket peut alors créer une intervention. Quand le ticket disparaît (plus aucune référence), ses interventions disparaissent avec lui.

Dans la pratique, la frontière entre association simple et agrégation est floue, et beaucoup de concepteurs ne dessinent plus de losange vide. La composition, elle, a une vraie conséquence sur le code : retiens-la.

Classe d'association

Entre Poste et Logiciel, l'association est « plusieurs à plusieurs », et elle porte une donnée : la date d'installation. Cette date n'appartient ni au poste ni au logiciel, mais au couple. UML la place dans une classe d'association, reliée au trait par des pointillés. En C#, elle devient une classe ordinaire qui référence les deux objets :

csharp
class Installation
{
    public Installation(Poste poste, Logiciel logiciel, DateOnly date)
    {
        Poste = poste;
        Logiciel = logiciel;
        Date = date;
    }

    public Poste Poste { get; }
    public Logiciel Logiciel { get; }
    public DateOnly Date { get; }
}
csharp
// Dans la classe Poste
private readonly List<Installation> installations = new List<Installation>();
public IReadOnlyList<Installation> Installations => installations;

public Installation Installer(Logiciel logiciel, DateOnly date)
{
    Installation i = new Installation(this, logiciel, date);
    installations.Add(i);
    logiciel.Enregistrer(i);          // l'autre côté, si l'association est bidirectionnelle
    return i;
}
csharp
Logiciel vlc = new Logiciel("VLC");
Installation inst = pc.Installer(vlc, new DateOnly(2026, 11, 2));
Console.WriteLine($"{inst.Logiciel.Nom} installé sur {inst.Poste.NumInventaire} le {inst.Date:dd/MM/yyyy}");
text
VLC installé sur PC-0007 le 02/11/2026

C'est l'exact reflet de la table installation de la base parc, dont la clé primaire est le couple (id_poste, id_logiciel) : le passage du MCD au modèle relationnel suit la même logique, détaillée dans le cours Merise.

La dépendance

Une dépendance est une relation faible et ponctuelle : une classe utilise une autre dans une méthode (paramètre, variable locale, valeur de retour) sans la garder dans un champ. Une classe RapportMensuel qui reçoit une liste de tickets dans sa méthode Generer dépend de Ticket, mais ne lui est pas associée. On ne la dessine que si elle aide à comprendre : dans un sujet, une flèche en pointillés vers une classe signifie « elle s'en sert ».

Du code au diagramme

L'exercice inverse tombe aussi : on te donne du code, tu dessines (ou complètes) le diagramme. Procède membre par membre :

  1. Chaque classe devient un rectangle ; chaque champ ou propriété de type simple (string, int, DateOnly…) devient un attribut, avec sa visibilité.
  2. Chaque champ ou propriété du type d'une autre classe devient une association vers cette classe, pas un attribut. Son nom devient le rôle ; son caractère nullable donne 0..1, sinon 1.
  3. Chaque collection d'objets d'une autre classe donne une association de multiplicité * (ou 0..*).
  4. Si une classe crée les objets de la collection et ne les partage pas, c'est une composition.
  5. Chaque méthode publique devient une opération, avec ses paramètres et son type de retour.

Exercices

Exercice 1 sur 4 Du diagramme au code Réussi À revoir

Sorbier Logistique souscrit des contrats de maintenance. Voici l'extrait du diagramme de classes, au format texte :

  • Fournisseur : - nom : string, - email : string, + NouveauContrat(reference : string, dateFin : DateOnly, montantAnnuel : decimal, premierPoste : Poste) : ContratMaintenance, + MontantTotalActif(jour : DateOnly) : decimal
  • ContratMaintenance : - reference : string, - dateFin : DateOnly, - montantAnnuel : decimal, + EstActif(jour : DateOnly) : bool, + AjouterPoste(p : Poste) : void
  • Association Fournisseur 1 —— 0..* ContratMaintenance, navigable dans les deux sens (rôles fournisseur et contrats).
  • Association ContratMaintenance 0..* —— 1..* Poste, navigable du contrat vers les postes (rôle postes).

Un contrat est actif jusqu'à sa date de fin incluse. MontantTotalActif additionne les montants annuels des contrats actifs ce jour-là. AjouterPoste ignore un poste déjà couvert. Écris les deux classes ; la classe Poste existe déjà.

Voir la réponseMasquer la réponse de l'exercice 1
csharp
class Fournisseur
{
    private readonly List<ContratMaintenance> contrats = new List<ContratMaintenance>();

    public Fournisseur(string nom, string email)
    {
        Nom = nom;
        Email = email;
    }

    public string Nom { get; }
    public string Email { get; }
    public IReadOnlyList<ContratMaintenance> Contrats => contrats;

    public ContratMaintenance NouveauContrat(string reference, DateOnly dateFin, decimal montantAnnuel, Poste premierPoste)
    {
        ContratMaintenance c = new ContratMaintenance(reference, dateFin, montantAnnuel, this, premierPoste);
        contrats.Add(c);
        return c;
    }

    public decimal MontantTotalActif(DateOnly jour)
    {
        decimal total = 0;
        foreach (ContratMaintenance c in contrats)
        {
            if (c.EstActif(jour))
            {
                total += c.MontantAnnuel;
            }
        }
        return total;
    }
}

class ContratMaintenance
{
    private readonly List<Poste> postes = new List<Poste>();

    internal ContratMaintenance(string reference, DateOnly dateFin, decimal montantAnnuel,
                                Fournisseur fournisseur, Poste premierPoste)
    {
        Reference = reference;
        DateFin = dateFin;
        MontantAnnuel = montantAnnuel;
        Fournisseur = fournisseur;
        postes.Add(premierPoste);
    }

    public string Reference { get; }
    public DateOnly DateFin { get; }
    public decimal MontantAnnuel { get; }
    public Fournisseur Fournisseur { get; }
    public IReadOnlyList<Poste> Postes => postes;

    public bool EstActif(DateOnly jour) => jour <= DateFin;

    public void AjouterPoste(Poste p)
    {
        if (!postes.Contains(p))
        {
            postes.Add(p);
        }
    }
}

Les choix qui rapportent des points : la multiplicité 1 côté fournisseur devient une propriété non nullable fixée à la construction ; 1..* côté postes est garanti par le constructeur, qui exige un premier poste ; l'association bidirectionnelle reste cohérente parce que seul NouveauContrat crée un contrat (constructeur internal) et l'ajoute à la liste du fournisseur. Avec deux contrats de 1 200 € (fin au 31/12/2026) et 800 € (fin au 30/06/2026), MontantTotalActif renvoie 1 200 au 2 novembre 2026 et 2 000 au 1er mars 2026.

Tu avais trouvé ?

Exercice 2 sur 4 Agrégation ou composition ? Réussi À revoir

Pour chaque couple, choisis entre association simple, agrégation et composition, et justifie.

  1. Une commande de matériel et ses lignes de commande.
  2. Une salle de formation et les postes qui y sont installés.
  3. Une fiche de frais et ses frais hors forfait.
  4. Un ticket et le poste concerné.
Voir la réponseMasquer la réponse de l'exercice 2
  1. Composition : une ligne de commande n'existe que dans sa commande ; supprimer la commande supprime ses lignes.
  2. Agrégation (ou association simple) : un poste existe sans la salle et peut être déménagé dans une autre.
  3. Composition : un frais hors forfait appartient à une seule fiche et disparaît avec elle.
  4. Association simple, de multiplicité 0..1 côté poste : le ticket désigne le poste, mais ne le contient pas ; le poste existe avant et après le ticket.

Tu avais trouvé ?

Exercice 3 sur 4 Du code au diagramme Réussi À revoir

Dessine (ou décris) le diagramme de classes correspondant à ce code : classes, attributs avec visibilité, associations avec multiplicités et type de relation.

csharp
public class Formation
{
    private string intitule;
    private DateOnly date;
    private Employe formateur;
    private readonly List<Inscription> lesInscriptions = new();

    public void Inscrire(Employe e) { lesInscriptions.Add(new Inscription(e, DateTime.Now)); }
    public int NbInscrits() { return lesInscriptions.Count; }
}

public class Inscription
{
    private Employe participant;
    private DateTime dateInscription;
    private bool presence;

    public Inscription(Employe e, DateTime d) { participant = e; dateInscription = d; }
}
Voir la réponseMasquer la réponse de l'exercice 3
  • Formation : attributs - intitule : string, - date : DateOnly ; opérations + Inscrire(e : Employe) : void, + NbInscrits() : int.
  • Inscription : attributs - dateInscription : DateTime, - presence : bool ; opération + Inscription(e : Employe, d : DateTime) (le constructeur).
  • Formation → Employe : association de rôle formateur, multiplicité 1 côté Employe (champ non nullable).
  • Formation ◆→ Inscription : composition (losange plein côté Formation), multiplicité 0..* côté Inscription : la formation crée elle-même ses inscriptions dans Inscrire.
  • Inscription → Employe : association de rôle participant, multiplicité 1.

formateur et participant ne sont pas des attributs : ce sont des associations, parce que leur type est une classe du diagramme. C'est l'erreur la plus fréquente dans cet exercice.

Tu avais trouvé ?

Exercice 4 sur 4 Le lien à moitié fait Réussi À revoir

Ce code compile, mais l'application affiche des résultats contradictoires après un changement d'affectation. Trouve le problème et corrige AffecterA.

csharp
public class Poste
{
    public Employe? Utilisateur { get; private set; }
    public void AffecterA(Employe e) { Utilisateur = e; }
}

public class Employe
{
    private readonly List<Poste> postes = new();
    public IReadOnlyList<Poste> Postes => postes;
    internal void AjouterPoste(Poste p) => postes.Add(p);
    internal void RetirerPoste(Poste p) => postes.Remove(p);
}
Voir la réponseMasquer la réponse de l'exercice 4

L'association est bidirectionnelle, mais AffecterA ne met à jour qu'un côté : poste.Utilisateur désigne Julie, alors que julie.Postes ne contient pas le poste. Et l'ancien utilisateur garde le poste dans sa liste.

csharp
public void AffecterA(Employe e)
{
    Utilisateur?.RetirerPoste(this);    // l'ancien utilisateur perd le poste
    Utilisateur = e;
    e.AjouterPoste(this);               // le nouveau le gagne
}

Les trois opérations forment un tout : elles sont dans une seule méthode, et les méthodes AjouterPoste et RetirerPoste restent internal pour qu'aucun code extérieur ne puisse modifier un seul côté.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Collections, génériques et LINQ : List, DictionaryRevoir le sommaire du cours