Modéliser : du texte au diagramme de classes
Séance 2 · ~2 périodes
Vous savez écrire une classe et faire tourner des tests. Reste une question qui arrive avant le code : quelles classes écrire ? Comment passer d'un énoncé en français à une liste de classes ?
C'est le rôle du diagramme de classes. Il est écrit en UML, un langage de dessin normalisé qui sert à représenter un programme. Ce dessin, vous le referez au tableau avec un collègue, et vous le retrouverez dans la documentation de la plupart des projets. Quand la documentation de Laravel dira qu'un Post a plusieurs Comment, vous saurez le dessiner.
À la fin de ce chapitre, vous serez capables de :
- extraire d'un énoncé en français les classes, les attributs et les méthodes candidates ;
- dessiner une classe en notation UML : compartiments, visibilités, types ;
- relier deux classes avec la bonne relation, ses deux rôles et ses multiplicités ;
- critiquer un modèle : repérer la classe qui fait tout et les tableaux qui cachent des classes.
Le code qui sent mauvais
Cinq minutes, en binôme. Ce code sent mauvais : un détail d'écriture y trahit un défaut de conception. Lisez-le et répondez à une seule question : combien de classes manquent ici ?
<?php
declare(strict_types=1);
class Game
{
public array $hero = ['name' => 'Arthur', 'hp' => 10];
public array $rooms = [['name' => 'Entrée', 'monster' => null]];
public array $items = [['name' => 'Épée', 'weight' => 2.0, 'room' => 0]];
public function play(): void
{
$this->hero['hp'] -= 3;
$this->rooms[0]['monster'] = ['name' => 'Gobelin', 'hp' => 5];
echo $this->hero['name'], ' : ', $this->hero['hp'], " PV\n";
echo count($this->items), " objet(s) au sol\n";
}
}
(new Game())->play();
Arthur : 7 PV
1 objet(s) au sol
Ce code tourne.
1. Lire un énoncé avec un crayon
La méthode est mécanique : vous relisez l'énoncé trois fois, avec un crayon.
| Passage | Ce que vous soulignez | Ce que ça devient |
|---|---|---|
| 1 | Les noms communs importants, ceux qui reviennent | des classes candidates |
| 2 | Les données : adjectifs, nombres, qualificatifs | des attributs |
| 3 | Les verbes : ce que le système doit savoir faire | des méthodes |
L'UML dit attribut là où PHP dit propriété. Même chose, deux mots.
Trois questions ensuite, pour réduire la liste :
- Ce nom a-t-il des données à lui ? Sinon, c'est un attribut. « Le héros a un nom » :
nameest une chaîne de caractères, pas une classeName. - Ce nom sert-il à autre chose qu'à l'affichage ? L'écran de jeu ne fait pas partie du modèle du jeu.
- Quelle classe détient les données de ce verbe ?
takeDamage()modifiehp: il va dansHero, pas dansGame.
Appliquez la méthode à une phrase du Donjon : « le héros explore un donjon composé de salles ; certaines salles abritent un monstre et contiennent des objets que le héros peut ramasser dans son sac ». Les noms donnent six candidats : héros, donjon, salle, monstre, objet, sac.
Le sac a des données à lui, un poids maximum précisé plus loin dans l'énoncé complet, et une liste d'objets. Il a aussi des actions : ajouter, retirer, peser. Ce sac devient donc une classe, Inventory. « Exploration » n'a aucune donnée propre : ce sera une méthode. Les verbes se placent ensuite. « Ramasser » a besoin du sac et de l'objet, donc il va dans Hero, qui possède le sac.
Ces six noms sont des pistes, pas encore un modèle. Le dessin les confirme ou les élimine.
2. La boîte de classe
Une classe se dessine dans un rectangle à trois compartiments : le nom, les attributs, les méthodes.
@startuml
class Hero {
+name: string
+strength: int
-hp: int
-maxHp: int
+takeDamage(amount: int): void
+heal(amount: int): void
+isAlive(): bool
}
@enduml
Les symboles :
| Symbole | Signification |
|---|---|
+ |
public : visible de partout |
- |
private : visible seulement dans la classe. Convention du cours : une propriété public private(set) se note aussi -, ce qui compte pour le modèle est que seule la classe l'écrit |
# |
protected : la classe et ses classes filles, au chapitre 6 |
~ |
visible dans le même paquet. PHP n'a pas cette visibilité, vous la croiserez dans d'autres cours |
nom: type |
un attribut et son type |
nom: type [0..1] |
un attribut facultatif, qui peut valoir null |
methode(param: type): retour |
une méthode, ses paramètres typés et son type de retour |
| souligné | membre statique. PlantUML le note {static} |
Une classe abstraite est une classe dont on ne fabrique jamais d'objet directement. Deux notations existent : le nom en italique, ou le stéréotype «abstract» au-dessus du nom. Le cours recommande le stéréotype : il se lit sans hésiter, au tableau comme à l'écran.
@startuml
abstract class Monster <<abstract>> {
+name: string
#hp: int
+{abstract} attack(): int
}
@enduml
Deux autres boîtes existent, l'interface et l'énumération. Elles arrivent au chapitre 7 avec leur code.
Astuce
Un diagramme de classes n'est pas une recopie de votre code. Vous y mettez ce qui aide à comprendre : les données importantes, les méthodes publiques, les relations. Les méthodes de lecture, d'écriture et d'affichage, comme __toString(), peuvent rester en dehors du dessin.
3. Les relations
Association : « est en lien avec »
Un trait plein entre deux boîtes. Une association représente un lien durable entre les objets de deux classes.
@startuml
class Hero
class Weapon
Hero "0..1 -wielder" -- "0..1 -weapon" Weapon : wields >
@enduml

Une association porte toujours quatre choses :
- Un nom, un verbe, avec un petit triangle qui donne le sens de lecture : « un héros wields une arme ».
- Deux rôles, un à chaque bout. Le rôle dit comment la classe d'en face appelle celle-ci : côté
Weapon, le rôleweaponest le nom queHerodonne à son arme. Dans le code, c'est la propriété$weapondeHero. - Une visibilité par rôle,
-,+ou#, la même que pour un attribut. Un rôle privé devient une propriété privée. - Deux multiplicités, une à chaque bout : combien d'objets participent au lien de ce côté.
Un rôle par côté, sans exception, agrégation et composition comprises. Dans le code, vous n'êtes pas obligé de programmer les deux sens du lien. Le chapitre 5 en code un seul : la salle connaît l'objet posé au sol, l'objet ne connaît pas la salle. Le modèle, lui, nomme les deux.
Les multiplicités :
| Multiplicité | Se lit | En PHP |
|---|---|---|
1 |
exactement un | une propriété obligatoire |
0..1 |
zéro ou un | une propriété qui peut valoir null |
* ou 0..* |
zéro ou plusieurs | un tableau, éventuellement vide |
1..* |
au moins un | un tableau jamais vide |
Trois combinaisons reviennent tout le temps. Vous les retrouverez telles quelles dans Laravel, où chaque relation entre deux modèles porte un nom de méthode.
Un à un. Chaque héros porte un sac, chaque sac a un porteur.

Un à plusieurs. Un donjon contient plusieurs salles, une salle n'a qu'un donjon. C'est la relation la plus fréquente : un Post a plusieurs Comment, un Comment appartient à un seul Post.

Plusieurs à plusieurs. Un héros accomplit des quêtes, une quête est accomplie par plusieurs héros. En base de données, ce lien demande une table intermédiaire, que Laravel appelle table pivot.

| Multiplicités | Exemple du Donjon | Dans Laravel |
|---|---|---|
1 — 1 |
Hero porte Inventory |
hasOne d'un côté, belongsTo de l'autre |
1 — 0..* |
Dungeon contient Room |
hasMany du côté 1, belongsTo du côté plusieurs |
0..* — 0..* |
Hero accomplit Quest |
belongsToMany des deux côtés, avec une table pivot |
Dépendance : « utilise »
Un trait pointillé fléché. Une classe a besoin d'une autre le temps d'un appel, sans la garder.
@startuml
class Hero
class Dice
Hero ..> Dice : «use»
@enduml
Dans le Donjon, une méthode de Hero reçoit un Dice en paramètre pour lancer un jet, puis l'oublie : le dé n'est rangé dans aucune propriété. C'est la relation la plus faible du lot. Dessinez-la seulement si elle aide à comprendre la méthode.
Agrégation et composition : « fait partie de »
Les deux disent qu'un objet fait partie d'un autre. Le losange se place du côté du tout. Elles ne disent pas la même chose sur les durées de vie.
@startuml
class Room
class Monster
class Hero
class Inventory
Room "0..1 -room" o-- "0..1 -monster" Monster : abrite >
Hero "1 -owner" *-- "1 -inventory" Inventory : porte >
@enduml

Agrégation {uml:agregation} : losange vide, « a un ». Le tout et la partie ont des vies indépendantes. Dans le Donjon, une Room a un Monster. Le gobelin existait avant qu'on ouvre la porte, et si la salle s'effondre il ira ailleurs. La norme UML ne garantit rien de plus qu'une annotation « tout et partie ». Convention du cours : la partie survit au tout et peut changer de tout.
Composition {uml:composition} : losange plein, « est fait de ». La partie est créée par le tout et meurt avec lui. Deux contraintes sont fixées par UML : la multiplicité côté tout vaut toujours 1, et une partie n'entre que dans une seule composition à la fois. Dans le Donjon, un Hero est fait, entre autres, d'un Inventory. Le sac est créé dans le constructeur du héros, personne d'autre ne le possède, et il disparaît avec lui.
En base de données, la même question se pose au moment de supprimer une ligne. La réponse est déjà dans le losange :
| Losange | Quand le tout est supprimé | Clé étrangère |
|---|---|---|
| vide, agrégation | la partie reste, son lien est vidé | ON DELETE SET NULL |
| plein, composition | la partie est supprimée avec lui | ON DELETE CASCADE |
Attention
Beaucoup de supports intervertissent les deux définitions et écrivent que l'agrégation est le lien fort. C'est l'inverse. Le test qui tranche : si je supprime le tout, la partie a-t-elle encore un sens toute seule ? Oui, c'est une agrégation. Non, c'est une composition.
Un indice fiable la plupart du temps : qui fabrique l'objet ? Si le tout fabrique la partie dans son constructeur, c'est une composition. Si la partie arrive de l'extérieur, c'est une agrégation ou une association. Cet indice se trompe parfois. Quand il contredit la phrase « est fait de », suivez la phrase. Le chapitre 5 code exactement ces deux cas.
Ce qui vient plus tard
Deux relations arrivent dans les chapitres suivants :
- la généralisation {uml:generalisation}, trait plein et triangle vide collé à la classe mère, qui dit « est un » :
Weaponest unItem. Chapitre 6. - la réalisation {uml:realisation}, trait pointillé et triangle vide collé à l'interface, qui dit « respecte le contrat » :
HeroréaliseFighter. Chapitre 7.
Au chapitre 6, vous verrez que beaucoup d'héritages auraient dû être une simple propriété.

Les quatre relations de ce chapitre, avec ce qu'il faut taper en PlantUML :
| Relation | Trait | Se lit | Durée de vie de la partie | En PlantUML |
|---|---|---|---|---|
| Association | {uml:association} | « est en lien avec » | indépendante | A -- B |
| Dépendance | {uml:dependance} | « utilise » | le temps d'un appel | A ..> B |
| Agrégation, losange vide | {uml:agregation} | « a un » | survit au tout, peut changer de tout | A o-- B, losange contre A |
| Composition, losange plein | {uml:composition} | « est fait de » | meurt avec le tout, exclusif | A *-- B, losange contre A |
En PlantUML, le tout s'écrit à gauche et le losange se dessine contre lui. Les multiplicités et les rôles se mettent entre guillemets, de chaque côté : Hero "1 -owner" *-- "1 -inventory" Inventory.
4. Deux modèles du Donjon
Même énoncé, deux diagrammes. Prenez trois minutes pour lister ce qui cloche dans le premier avant d'ouvrir la carte.
Un modèle à améliorer
@startuml
class Game {
-heroName: string
-heroHp: int
-rooms: array
-items: array
-monsters: array
-currentRoom: int
+play(): void
+move(direction: string): void
+fight(): void
+pickUp(itemName: string): void
+save(): void
+printScreen(): void
}
note bottom of Game : Une seule classe. Tout est dedans.
@enduml
Un meilleur modèle
@startuml
class Dungeon {
+name: string
+entrance(): Room
}
class Room {
+name: string
+drop(item: Item): void
+take(): Item [0..1]
+describe(): string
}
class Monster {
+name: string
-hp: int
+attack(): int
}
class Hero {
+name: string
+strength: int
-hp: int
-maxHp: int
+takeDamage(amount: int): void
+heal(amount: int): void
+isAlive(): bool
+attack(target: Monster): void
}
class Inventory {
-maxWeight: float
+add(item: Item): bool
+remove(name: string): void
+totalWeight(): float
}
class Item {
+name: string
+weight: float
}
Dungeon "1 -dungeon" *-- "1..* -rooms" Room : contient >
Room "0..1 -room" o-- "0..1 -monster" Monster : abrite >
Room "0..1 -floor" o-- "0..1 -loot" Item : expose >
Hero "1 -owner" *-- "1 -inventory" Inventory : porte >
Inventory "0..1 -bag" o-- "0..* -items" Item : contient >
Hero "0..1 -visitor" -- "0..1 -location" Room : explore >
@enduml
Lisez-le en français : « un donjon est fait d'au moins une salle ; une salle a au plus un monstre et a au plus un objet au sol ; un héros est fait d'un inventaire ; un inventaire a zéro ou plusieurs objets ; un héros explore au plus une salle à la fois. »
Le combat n'est pas un trait : un héros attaque un monstre le temps d'un appel, attack(target: Monster), puis l'oublie. C'est une dépendance, et elle n'a pas besoin d'être dessinée.
currentRoom: int a disparu. Il est remplacé par l'association explore entre Hero et Room : le héros connaît sa salle, pas un numéro de case. Item apparaît des deux côtés, en agrégation. C'est voulu : une épée posée au sol passe dans le sac sans changer d'identité. Le diagramme n'interdit pas qu'un objet soit au sol et dans le sac en même temps. C'est une règle du jeu, et c'est le code qui la fera respecter. Un diagramme montre la structure, pas toutes les règles.
5. Dessiner
Commencez toujours sur papier, au crayon. Une feuille, une gomme, des boîtes vides, puis les traits. Vous effacez un losange en deux secondes, et vous voyez tout le modèle d'un coup. C'est le meilleur outil pour une première version, et c'est ce que vous ferez au tableau avec votre binôme.
Pour mettre le dessin au propre, trois outils :
| Outil | Ce que c'est | Quand |
|---|---|---|
| Modelio | un logiciel libre avec une interface graphique, qui produit des diagrammes conformes à UML 2 | recommandé pour vos rendus : rôles, multiplicités et losanges sont posés à la souris et toujours corrects |
| StarUML | un autre logiciel graphique, plus léger | si Modelio ne s'installe pas chez vous |
| PlantUML | un diagramme écrit en texte, comme les blocs de ce chapitre, rendu par un site ou une extension de votre éditeur | quand vous voulez versionner le diagramme avec le code |
Mermaid est interdit dans ce cours : il ne respecte pas le standard UML 2. Ses associations n'ont ni rôles ni sens de lecture corrects, et ses losanges sont approximatifs. Un diagramme Mermaid sera refusé.
En PlantUML, le squelette minimal tient en trois lignes :
@startuml
Hero "1 -owner" *-- "1 -inventory" Inventory : porte >
@enduml
Le rendu attendu pour l'exercice ci-dessous est au choix : une photo nette de votre dessin au propre, un export Modelio ou StarUML, ou un fichier PlantUML.
À retenir
- Les noms de l'énoncé donnent les classes candidates, les données les attributs, les verbes les méthodes. Un nom sans donnée ni comportement n'est pas une classe.
- Une boîte UML a trois compartiments,
+ - #pour la visibilité,nom: typepartout, souligné pour statique,«abstract»pour une classe abstraite. - Toute relation porte deux rôles avec leur visibilité et deux multiplicités. Une association porte en plus un nom et son sens de lecture.
- Agrégation, losange vide, « a un » : la partie survit au tout, en base
ON DELETE SET NULL. Composition, losange plein, « est fait de » : la partie meurt avec le tout, multiplicité1côté tout, en baseON DELETE CASCADE. - Une classe qui sait tout faire et un
arraysans contenu déclaré sont deux signaux qu'il manque des classes.
Exercice : la ludothèque
Modélisez le système suivant. Rendu attendu : un diagramme de classes avec les attributs typés, les méthodes publiques utiles, les relations avec leurs deux rôles et toutes les multiplicités. Pas de code. Papier d'abord, mise au propre ensuite. Comptez 40 minutes.
Un club de quartier gère une ludothèque. Elle possède un catalogue de jeux de société : chaque jeu a un titre, un éditeur, une durée moyenne en minutes, un nombre de joueurs minimum et un nombre maximum. La ludothèque possède plusieurs exemplaires physiques d'un même jeu ; chaque exemplaire a un code d'inventaire et un état, neuf, bon ou usé. Les membres du club ont un nom, un e-mail et une date d'inscription. Un membre peut emprunter jusqu'à trois exemplaires à la fois. Un prêt enregistre l'exemplaire emprunté, le membre, la date de sortie et la date de retour prévue ; il permet de savoir s'il est en retard. Un exemplaire ne peut être prêté qu'à un seul membre à la fois. Si la ludothèque ferme, ses exemplaires n'ont plus de raison d'être ; les membres, eux, existaient avant elle.
Cinq questions à vous poser pendant le travail, dans l'ordre :
- Combien de classes ? Comptez-les avant de dessiner.
- Où sont les compositions ? Cherchez dans l'énoncé les parties qui disparaissent avec le tout.
- Le prêt est-il une classe ou un simple trait ? Regardez s'il a des données à lui.
- Comment traduisez-vous « jusqu'à trois exemplaires à la fois » ?
- Où mettez-vous la méthode qui dit si un prêt est en retard ?