Cours 4 · Robustesse et modélisationLeçon 2 sur 2
Modélisation UML
5 h de lecture9 sections Version PDF
Diagramme de classes : attributs, méthodes, visibilité, associations et multiplicités, héritage et implémentation, agrégation et composition ; du diagramme au code, et retour.
Sept chapitres ont construit un vocabulaire : classe, attribut, visibilité, héritage, interface, composition. Il manque un moyen de montrer tout cela sans faire lire le code.
C'est ce que fait un diagramme de classes. Un programme de dix classes se lit en une minute sur un schéma, contre une heure dans les fichiers — et surtout, il se discute : c'est l'artefact qu'on met au tableau quand trois personnes doivent se mettre d'accord avant d'écrire.
UML compte quatorze types de diagrammes. Ce chapitre n'en traite qu'un, celui de classes, parce qu'il est le seul qu'on emploie vraiment en L2 — et parce qu'il est en correspondance directe avec le code.
La classe
┌─────────────────────────────┐│ Livre │ ← nom (en italique si abstraite)├─────────────────────────────┤│ - titre : String │ ← attributs│ - disponible : boolean ││ + NB_MAX : int │ ← souligné si static├─────────────────────────────┤│ + emprunter(qui : String) │ ← méthodes│ : boolean ││ + estDisponible() : boolean ││ - verifier() : void │└─────────────────────────────┘Trois compartiments : le nom, les attributs, les méthodes. Les deux derniers peuvent être omis quand ils n'apportent rien au propos — un diagramme n'est pas une transcription.
Les symboles de visibilité reprennent exactement le chapitre 3 :
| Symbole | Visibilité |
|---|---|
- | private |
+ | public |
# | protected |
~ | paquetage (par défaut) |
Deux conventions typographiques valent d'être connues, car elles se lisent d'un coup d'œil :
souligné signifie static, italique signifie abstrait — pour une classe comme pour
une méthode.
Les relations
C'est le cœur du chapitre, et chaque trait a un sens précis.
Livre ──────────▷ Document héritage (triangle creux, trait plein)Livre ┄┄┄┄┄┄┄┄┄▷ Empruntable réalisation (triangle creux, pointillés)Commande ◇────── Article agrégation (losange creux)Maison ◆────── Piece composition (losange plein)Etudiant ─────── Cours association (trait simple)Facture ┄┄┄┄┄┄> Calculatrice dépendance (flèche pointillée)L'héritage — triangle creux, trait plein, pointé vers le parent — traduit extends.
C'est le « est-un » du chapitre 5, avec le même critère de substituabilité.
La réalisation — même triangle, mais en pointillés — traduit implements. La distinction
visuelle est délibérée : hériter et implémenter ne sont pas la même chose.
L'association est un simple trait : deux classes se connaissent. En pratique, cela devient un attribut d'un côté ou des deux.
L'agrégation et la composition sont deux associations particulières, et leur différence est la seule subtilité du chapitre. Toutes deux expriment « a-un » ; ce qui les sépare est la durée de vie.
Dans une agrégation (losange creux), la partie survit au tout. Une Commande agrège des
Article : supprimer la commande ne supprime pas les articles, qui existaient avant et
continueront après.
Dans une composition (losange plein), la partie meurt avec le tout, et n'appartient qu'à
lui. Une Maison compose ses Piece : détruire la maison détruit les pièces, et une pièce
n'appartient pas à deux maisons.
Le test pratique : « si je supprime le tout, la partie a-t-elle encore un sens ? » Oui, agrégation ; non, composition.
Enfin la dépendance — flèche pointillée — signale un usage passager : une classe reçoit l'autre en paramètre ou l'emploie localement, sans la stocker.
Multiplicités
Un chiffre à chaque extrémité dit combien d'objets participent.
Bibliotheque 1 ◆────── 0..* LivreLivre * ─────── 0..1 Lecteur| Notation | Sens |
|---|---|
1 | exactement un |
0..1 | zéro ou un — donc peut être null |
* ou 0..* | zéro ou plusieurs |
1..* | au moins un |
2..4 | entre deux et quatre |
Les multiplicités sont la partie la plus utile du diagramme, et la plus souvent oubliée. Elles
répondent à des questions que le code ne pose pas explicitement : un livre peut-il être emprunté
par deux lecteurs à la fois ? un lecteur sans emprunt est-il légal ? Un 0..1 du côté
Lecteur répond « non » et « oui », et impose une décision qu'on aurait sinon prise par
accident.
Voici ces relations réunies dans un diagramme interactif. Chaque trait porte son symbole UML —
triangle d'héritage, losange de composition ou d'agrégation — et ses multiplicités ; déplacez les
classes pour démêler les liens, et retrouvez la composition Bibliotheque ◆ Livre, l'héritage
Livre ▷ Document et la réalisation de l'interface Empruntable.
Diagramme · Un diagramme de classes et ses relations
- Document — attributs : - titre : String
- Empruntable — méthodes : + emprunter() : boolean
- Livre — attributs : - disponible : boolean — méthodes : + emprunter() : boolean
- Bibliotheque — attributs : - livres : List<Livre>
- Lecteur — attributs : - nom : String
- Commande
- Article — attributs : - prix : double
Relations
- Livre ▷ hérite Document
- Livre ▷ réalise Empruntable
- Bibliotheque 1 ◆ compose 0..* Livre
- Livre emprunté par 0..1 Lecteur
- Commande ◇ agrège 0..* Article
Du diagramme au code
La traduction est presque mécanique, et c'est ce qui rend le diagramme utile plutôt que décoratif.
| Sur le diagramme | Dans le code Java |
|---|---|
- titre : String | private String titre; |
| Attribut souligné | static |
| Classe en italique | abstract class |
| Triangle creux plein | extends |
| Triangle creux pointillé | implements |
Association 1 | un attribut du type visé |
Association 0..1 | un attribut, qui peut être null |
Association * | List<Type> ou Map<Clé, Type> |
| Composition | le tout crée ses parties dans son constructeur |
| Agrégation | les parties sont reçues en paramètre |
Les deux dernières lignes méritent qu'on s'y arrête, car elles transforment une nuance sémantique en différence de code visible :
class Maison { /* COMPOSITION */ private final List<Piece> pieces = new ArrayList<>(); public Maison(int nb) { for (int i = 0; i < nb; i++) pieces.add(new Piece()); /* elle les crée */ }} class Commande { /* AGRÉGATION */ private final List<Article> articles; public Commande(List<Article> articles) { this.articles = articles; /* elle les reçoit */ }}Le chemin inverse compte autant : savoir lire du code existant et en tirer un diagramme est ce qu'on fait le premier jour sur un projet qu'on n'a pas écrit. C'est aussi ce que font les outils de rétro-ingénierie des ateliers de développement.
Ce qu'un diagramme ne dit pas, et ce qu'il ne faut pas en faire
Trois limites, pour ne pas prendre le dessin pour le programme.
Il ne dit rien du comportement. L'ordre des appels, les conditions, les boucles n'y figurent pas — c'est le rôle d'autres diagrammes UML, hors programme, comme celui de séquence.
Il vieillit. Un diagramme qui n'est pas maintenu devient un mensonge, plus nuisible qu'une absence de documentation. D'où deux usages viables : le dessin jetable au tableau avant de coder, et le diagramme régénéré depuis le code.
Il ne doit pas tout montrer. Un diagramme de quarante classes avec tous les getters est illisible et n'aide personne. On dessine ce qui répond à la question du moment : les relations principales, sans les accesseurs, sans les classes utilitaires.
Le modéliser-avant-tout des années 1990 a d'ailleurs largement été abandonné. Ce qui reste, et qui est ce qu'on enseigne ici : un croquis de cinq à dix classes, fait avant d'écrire, pour se mettre d'accord.
Quelle est la différence entre agrégation et composition, et comment la trancher ?
Sur un diagramme, la relation Livre * ── 0..1 Lecteur. Que dit-elle, et comment se traduit-elle ?
À vous
L'exercice fait les deux trajets, et c'est le second qui est formateur.
Du diagramme au code : on vous donne un diagramme décrit sous forme de données — classes,
attributs, relations, multiplicités — et vous écrivez le générateur qui en produit les squelettes
Java. Il devra traduire correctement 1, 0..1 et *, et distinguer agrégation et composition
par la façon dont les parties arrivent.
Du code au diagramme : on vous donne des classes, et vous en extrayez les relations. Un cas
est ambigu — un attribut de type List peut être une agrégation ou une composition — et c'est
en regardant qui crée les objets que vous trancherez.
Un dernier contrôle vérifie les multiplicités sur des données : un livre à deux emprunteurs simultanés doit être signalé comme violant le diagramme.
Générez du Java depuis un diagramme, puis retrouvez les relations à partir du code — agrégation ou composition ?
// ── Un diagramme, décrit sous forme de données ──────────────────────────── const DIAGRAMME = { classes: [ { nom: "Document", abstraite: true, attributs: [{ v: "-", nom: "titre", type: "String" }], methodes: [{ v: "+", nom: "decrire", type: "String", abstraite: true }] }, { nom: "Livre", herite: "Document", implemente: ["Empruntable"], attributs: [{ v: "-", nom: "isbn", type: "long" }, { v: "+", nom: "NB_MAX", type: "int", statique: true }], methodes: [{ v: "+", nom: "decrire", type: "String" }] }, { nom: "Bibliotheque", attributs: [{ v: "-", nom: "nom", type: "String" }], methodes: [] }, { nom: "Lecteur", attributs: [{ v: "-", nom: "nom", type: "String" }], methodes: [] }, ], relations: [ { de: "Bibliotheque", vers: "Livre", type: "composition", multiplicite: "*" }, { de: "Lecteur", vers: "Livre", type: "agregation", multiplicite: "*" }, { de: "Livre", vers: "Lecteur", type: "association", multiplicite: "0..1" }, ], }; // ── 1. Du diagramme au code ─────────────────────────────────────────────── function versJava(d) { const lignes = []; for (const c of d.classes) { let entete = (c.abstraite ? "abstract " : "") + "class " + c.nom; if (c.herite) entete += " extends " + c.herite; if (c.implemente) entete += " implements " + c.implemente.join(", "); lignes.push("public " + entete + " {"); const VISIBILITE = { "-": "private", "+": "public", "#": "protected", "~": "" }; for (const a of c.attributs) { lignes.push(" " + [VISIBILITE[a.v], a.statique ? "static final" : "", a.type, a.nom] .filter(Boolean).join(" ") + ";"); } // ← à écrire : traduire les relations dont cette classe est l'origine. // « 1 » et « 0..1 » donnent un attribut simple, « * » donne une List. lignes.push("}"); lignes.push(""); } return lignes.join("\n"); } // ── 2. Du code au diagramme ─────────────────────────────────────────────── const SOURCES = { Panier: [ "class Panier {", " private List<Article> articles;", " Panier(List<Article> articles) { this.articles = articles; }", // reçus "}", ], Maison: [ "class Maison {", " private List<Piece> pieces = new ArrayList<>();", " Maison(int n) { for (int i=0;i<n;i++) pieces.add(new Piece()); }", // créées "}", ], }; function versDiagramme(nom, source) { const texte = source.join("\n"); const champ = /private List<(\w+)>/.exec(texte); if (!champ) return { de: nom, type: "aucune relation détectée" }; // ← à écrire : agrégation ou composition ? Regardez QUI CRÉE les objets. return { de: nom, vers: champ[1], type: "à déterminer", multiplicite: "*" }; } // ── À VOUS ──────────────────────────────────────────────────────────────── // 1. Complétez versJava pour les trois multiplicités. // 2. Complétez versDiagramme : le critère est « qui crée les objets ». // 3. Écrivez un contrôle de multiplicité : un livre emprunté par deux // lecteurs simultanément viole le 0..1 du diagramme. console.log(versJava(DIAGRAMME));
En travaux pratiques
Le diagramme comme outil de décision
Dessiner AVANT de coder, puis rétro-modéliser le code écrit, et mesurer l'écart entre les deux — c'est l'écart qui est instructif, pas le diagramme.
- Les TP 1 à 7 : la médiathèque en état de marche
- Un outil de diagramme, ou du papier
- 1. Rétro-modéliser
Dessinez le diagramme de classes de votre médiathèque telle qu'elle est aujourd'hui. Classes, attributs, méthodes publiques, et les relations entre elles.
- 2. Nommer les relations
Pour chaque trait de votre diagramme, dites s'il s'agit d'un héritage, d'une composition, d'une agrégation ou d'une simple association, et justifiez.
- 3. Les cardinalités
Annotez chaque association de ses cardinalités. Vérifiez ensuite dans le code que chacune est réellement appliquée.
- 4. Ce que le diagramme révèle
Repérez sur votre dessin : une classe qui connaît trop de monde, une relation dans les deux sens, un cycle de dépendances. Corrigez-en un dans le code.
- 5. Concevoir en amont
Nouvelle exigence : gérer les adhérents et les emprunts avec dates. Dessinez le modèle AVANT d'écrire une ligne. Discutez-en à deux, dessin contre dessin.
- 6. Le diagramme de séquence
Dessinez le déroulement d'un emprunt : qui appelle qui, dans quel ordre, et où la règle métier est vérifiée. Trouvez si la vérification est au bon endroit.
- 7. Coder puis comparer
Implémentez ce que vous avez dessiné, puis rétro-modélisez à nouveau. Listez les écarts et dites, pour chacun, qui avait raison — le dessin ou le code.
- Vous distinguez composition et agrégation sur un exemple de votre propre code
- Vos cardinalités sont réellement appliquées, ou bien vous savez dire où elles ne le sont pas
- Vous listez au moins deux écarts entre le dessin initial et le code final
Ce que la suite en fait
Le bloc V met tout en œuvre. Le chapitre 9 donne les collections qui matérialisent les
multiplicités * — List, Map, Set — et l'on verra que le choix entre elles est lui-même
une décision de modélisation : un Set interdit les doublons, une Map impose une clé unique.
Le chapitre 10 partira d'un énoncé en français pour arriver à un diagramme, puis au code. La notation de ce chapitre y sera l'outil de travail, et non plus le sujet.
À retenir
Vous avez parcouru les 9 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.