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.
| Relation | Sens | Notation UML | Exemple GestParc |
|---|---|---|---|
| Dépendance | une classe en utilise une autre ponctuellement, sans la mémoriser | flèche en pointillés | Rapport.Generer(List<Ticket>) reçoit des tickets en paramètre |
| Association | une classe connaît durablement une autre | trait plein, multiplicités aux extrémités | un poste est affecté à un employé |
| Agrégation | association « tout et parties », les parties vivent sans le tout | losange vide côté tout | un service regroupe des employés |
| Composition | agrégation forte : les parties naissent et meurent avec le tout | losange plein côté tout | un ticket contient ses interventions |
| Héritage (généralisation) | une classe est une sorte de une autre | flèche à triangle vide | un 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.
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 UML | Visibilité | C# |
|---|---|---|
+ | publique | public |
- | privée | private |
# | protégée | protected |
~ | paquetage | internal |
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é | Lecture | Traduction en C# |
|---|---|---|
1 | exactement un | une propriété non nullable : Service Service |
0..1 | zéro ou un | une propriété nullable : Employe? Utilisateur |
* ou 0..* | zéro, un ou plusieurs | une collection : List<Intervention> exposée en IReadOnlyList<Intervention> |
1..* | au moins un | une collection, et un constructeur qui exige le premier élément |
2..5 | entre deux et cinq | une 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é.
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;
}
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é")}");
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).
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.
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;
}
}
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}");
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 ? | oui | non |
| Une partie peut appartenir à plusieurs touts ? | oui | non, à un seul |
| Qui crée les parties ? | le code extérieur, qui les passe au tout | le tout lui-même |
| Supprimer le tout supprime les parties ? | non | oui |
| Exemple | un service et ses employés : supprimer le service ne licencie personne | un 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 :
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));
}
}
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}");
}
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 :
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; }
}
// 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;
}
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}");
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 :
- Chaque classe devient un rectangle ; chaque champ ou propriété de type simple (
string,int,DateOnly…) devient un attribut, avec sa visibilité. - 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, sinon1. - Chaque collection d'objets d'une autre classe donne une association de multiplicité
*(ou0..*). - Si une classe crée les objets de la collection et ne les partage pas, c'est une composition.
- 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) : decimalContratMaintenance:- 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ôlesfournisseuretcontrats). - Association
ContratMaintenance 0..* —— 1..* Poste, navigable du contrat vers les postes (rôlepostes).
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
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.
- Une commande de matériel et ses lignes de commande.
- Une salle de formation et les postes qui y sont installés.
- Une fiche de frais et ses frais hors forfait.
- Un ticket et le poste concerné.
Voir la réponseMasquer la réponse de l'exercice 2
- Composition : une ligne de commande n'existe que dans sa commande ; supprimer la commande supprime ses lignes.
- Agrégation (ou association simple) : un poste existe sans la salle et peut être déménagé dans une autre.
- Composition : un frais hors forfait appartient à une seule fiche et disparaît avec elle.
- Association simple, de multiplicité
0..1cô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.
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ôleformateur, 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 dansInscrire.Inscription→Employe: association de rôleparticipant, 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.
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.
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é ?