Le diagramme de classes & l'architecture logique
Même notation que le modèle du domaine, et un tout autre document. Celui-ci ne décrit plus le monde : il décrit le logiciel qu'on va écrire, classe par classe, trait par trait. Et chaque chose qu'il ajoute est une décision — donc quelque chose qu'on peut prendre autrement, et qu'il faudra savoir défendre.
Jusqu'ici, tout ce que tu as dessiné pouvait être validé : le client regarde le modèle du domaine et dit oui ou non, parce qu'il parle de son métier. À partir de maintenant, plus personne ne peut te dire si tu as raison. Le client n'a aucun avis sur la classe qui doit calculer un total, et il n'a pas à en avoir un.
C'est ce basculement qui définit la conception, et il change la nature de ce que tu produis : un modèle du domaine se fait valider, un diagramme de conception se défend. Toutes les règles de ce module servent la même chose — pouvoir justifier chaque ligne à quelqu'un qui aurait pu la tracer autrement.
Ce que la conception ajoute, et pourquoi c'est une décision
Le diagramme de classes de conception — on l'abrège DCC, et Larman l'appelle design class diagram — emploie exactement la même notation que le modèle du domaine. Des boîtes, des traits, des nombres aux extrémités. C'est précisément ce qui le rend piégeux : rien dans le dessin n'annonce qu'on a changé de monde.
Ce qui a changé, c'est le lecteur. La section Du domaine à la conception du module Le modèle du domaine dresse le tableau des sept différences, et sa dernière ligne explique les six autres : le modèle du domaine se montre au client, le DCC ne se montre qu'à l'équipe. On ne va pas le refaire ici. On va faire l'inverse : regarder chacun de ces ajouts comme ce qu'il est vraiment — une décision.
L'architecte commence par un croquis : des volumes, des pièces, leur enchaînement. Il le montre au client, qui dit « la cuisine devrait donner sur le jardin ». Puis il fait les plans d'exécution : épaisseurs de murs, sections de poutres, positions des prises. Le client ne les regarde pas — il n'y comprendrait rien, et ce n'est pas à lui de choisir une section de poutre.
Mais l'ingénieur en structure, lui, la discutera. Ce n'est pas « plus précis » : c'est discutable par d'autres gens, sur d'autres critères. Un DCC se relit comme ça — par quelqu'un qui aurait pu le faire autrement.
Reprends la classe Emprunt du module précédent. En analyse, elle porte
datePret et dateRetourPrevue, et rien d'autre. En conception,
elle porte - datePret : Date. Trois choses se sont ajoutées à cette seule
ligne, et chacune est un choix :
-
Le type
Date— on aurait pu prendre un entier de secondes, ou trois entiers jour/mois/année. Le choix engage tout le reste : comparer deux dates, les afficher, les stocker. -
La visibilité
-— on décide que personne d'autre qu'Empruntne touche à cette date. Ce n'est pas une évidence : c'est l'affirmation qu'on veut pouvoir changer sa représentation plus tard sans casser personne. - Le fait qu'elle soit là — un attribut du domaine peut très bien ne jamais devenir un attribut de classe. S'il se recalcule à partir des autres, on écrit une opération à la place.
Voilà la vraie nature du passage à la conception. Le modèle du domaine te disait quels mots existent ; le DCC répond à des questions que le domaine ne pose pas, et à chacune il y avait plusieurs réponses acceptables. C'est aussi pour ça qu'un examen peut te demander de justifier un choix de conception, ce qu'il ne demanderait jamais d'un modèle du domaine.
Une chose, en revanche, ne change pas et ne doit pas changer : les
noms. Si le métier dit « emprunt », la classe s'appelle
Emprunt, du modèle du domaine jusqu'au fichier Emprunt.java.
C'est tout le bénéfice d'avoir fixé le vocabulaire au début, et il se perd en une seule
renommage malheureux.
Croire que le DCC est le modèle du domaine avec des parenthèses en plus. C'est l'image qu'on se fait naturellement — recopier les boîtes, ajouter des méthodes, c'est fini. Elle est fausse dans les trois sens.
Le DCC ajoute des classes que le métier n'a jamais eues — contrôleurs, fenêtres, accès aux données. Il en retire : un concept du domaine qu'aucun cas d'utilisation n'exerce n'a aucune raison de devenir une classe. Et il modifie ce qu'il garde : une association bidirectionnelle du domaine devient très souvent unidirectionnelle en conception, parce que le logiciel n'a besoin de la parcourir que dans un sens.
La boîte, ligne par ligne
La boîte a trois compartiments : le nom, les attributs, les opérations. Tu les as déjà vus au module Le modèle du domaine, où le troisième restait vide. Ici il se remplit, et chaque ligne des deux compartiments du bas suit une grammaire précise. C'est cette grammaire que l'examen te demandera d'écrire sans hésiter — et sans notes.
Visibilité, nom, type
VISIBILITÉ + public accessible de partout
- privé accessible dans la classe seule
# protégé la classe et ses sous-classes
~ paquetage les classes du même paquetage
ATTRIBUT visibilité nom : Type [multiplicité] = valeurParDefaut
- solde : double = 0.0
- lignes : LigneCommande [1..*]
OPÉRATION visibilité nom(param : Type, ...) : TypeRetour
+ calculerTotal() : double
+ ajouterLigne(plat : Plat, quantite : int) : void
souligné = membre de CLASSE italique = ABSTRAIT
La notation d'un membre de classe en UML. On l'écrit pour chaque ligne des deux compartiments du bas ; on la reconnaît au fait que tout y est facultatif sauf le nom et la visibilité — et que c'est la visibilité qu'on oublie.
Quelques précisions qui reviennent en examen. La multiplicité d'un
attribut, entre crochets, dit qu'il en contient plusieurs :
lignes : LigneCommande [1..*] annonce une collection, et c'est souvent une
autre façon d'écrire une association — on y revient dans la section
« La navigabilité, ou le trait qui devient un attribut ».
Le soulignement marque ce qui appartient à la classe et non aux objets —
static en Java. + nombreDeComptes() : int souligné veut dire
qu'on l'appelle sur la classe CompteBancaire elle-même, sans avoir de compte
sous la main. L'italique, lui, marque l'abstrait : une classe en
italique ne s'instancie pas, une opération en italique n'a pas de corps et doit être
redéfinie plus bas. Ces deux marques sont typographiques et non symboliques, ce qui est
leur seul défaut : sur une copie manuscrite, souligne franchement.
Cette boîte est censée être une classe de conception. Relève les quatre lignes qui ne respectent pas la notation.
CompteBancaire
-----------------------------------------
numero : String
- solde
# tauxInteret : double
-----------------------------------------
+ deposer(montant : double)
- validerMontant(m : double) : boolean
+ nombreDeComptes : int
Ligne 1 — numero : String : la visibilité manque.
C'est la faute la plus coûteuse du lot, parce qu'elle se répète : un correcteur
qui la trouve une fois la cherche partout.
Ligne 2 — - solde : le type manque. En analyse, il
était facultatif ; en conception, il ne l'est plus — c'est justement l'une des
trois marques du logiciel.
Ligne 4 — + deposer(montant : double) : le type de
retour manque. S'il n'y en a pas, on écrit : void ; le laisser en
blanc veut dire « je n'ai pas décidé ».
Ligne 6 — + nombreDeComptes : int : elle est dans
le compartiment des opérations mais s'écrit comme un attribut, sans parenthèses. Si
c'est un compteur global, il descend d'un compartiment et se souligne ; si c'est
une opération, elle prend ses parenthèses.
Remplir le troisième compartiment d'accesseurs. Écrire
+ getSolde(), + setSolde(), + getNumero(),
+ setNumero() pour chaque attribut donne une boîte impressionnante et vide
de sens : on a recopié le compartiment du milieu en deux fois plus long.
Larman le dit sans détour : les accesseurs triviaux n'ont pas leur place sur un
diagramme. Le compartiment des opérations sert à montrer ce que la classe sait
faire — estEnRetard(), calculerTotal(),
prolonger(). Si ta boîte ne contient que des get et des
set, tu n'as pas encore conçu : tu as déclaré des données.
D'où viennent les opérations
Voici la question que les cours sautent presque toujours, et sur laquelle on bute dès
qu'on est devant une copie blanche : qui décide qu'Emprunt a une
opération estEnRetard() ? Pas le client, on vient de le voir. Pas
le modèle du domaine, qui n'en porte aucune. Il y a une réponse, et elle vient en trois
temps.
Premier temps : les opérations système. La section
Les
opérations système du module
Cas d'utilisation & DSS le
dit déjà : chaque flèche entrante d'un DSS est une opération que le système doit
rendre, et cette liste est le contrat que la conception doit réaliser. C'est
l'entrée de ce module. Si ton DSS d'emprunt donne
identifierLecteur(numero), identifierLivre(isbn) et
confirmerEmprunt(), tu as trois obligations, et pas une de plus.
Deuxième temps : une opération système n'est pas une méthode de
classe. C'est la marche qu'on rate. confirmerEmprunt() est une
opération du système entier, vu de l'extérieur comme une boîte noire. Elle doit
maintenant être attribuée à une classe, qui devra en appeler d'autres
pour faire le travail — et chacun de ces appels devient à son tour une opération de la
classe appelée.
Troisième temps : c'est le diagramme d'interaction qui fait cette distribution. On y dessine les objets qui collaborent et les messages qu'ils s'envoient ; tout message reçu par un objet est une opération de sa classe. C'est mécanique, et c'est la seule source complète des opérations.
Le cours te fait dessiner le diagramme de classes cette semaine et Les diagrammes d'interaction la semaine prochaine. Lu comme une recette, c'est illogique : on te demande les opérations avant l'outil qui les trouve.
Ce n'est pas une erreur du plan, c'est la réalité du travail. Le DCC se remplit par allers-retours : on pose d'abord les classes et les attributs, qui viennent du domaine et qu'on connaît déjà ; on ajoute les opérations qu'on peut déduire sans interaction ; puis chaque diagramme d'interaction dessiné renvoie une poignée d'opérations à inscrire dans les boîtes. Le diagramme de classes est le seul du cours qui n'est jamais « fini » avant la fin.
Reste ce qu'on peut écrire tout de suite, sans avoir tracé un seul message, et qui suffit largement pour un premier livrable :
-
Les opérations qui découlent des attributs. Un
Empruntqui connaît sadateRetourPrevuesait dire s'il est en retard. UneCommandequi connaît ses lignes sait calculer son total. La règle qui les trouve est simple : quelle question peut-on poser à cet objet à laquelle lui seul peut répondre ? -
Les opérations qui viennent d'une règle du métier.
« Un lecteur qui doit plus de 5 $ ne peut pas emprunter » donne
+ estEnRegle() : booleansurLecteur. La règle existait dans l'énoncé ; elle n'avait simplement nulle part où aller en analyse. - Les opérations système, posées sur un contrôleur. On leur donne une classe d'accueil — voir la section « Les classes que le métier n'a jamais eues » — quitte à ce que son contenu se précise plus tard.
Voici les opérations système tirées du DSS « Emprunter un livre ». Pour chacune, dis quelle classe la reçoit, et nomme une opération qu'elle devra elle-même appeler.
1. identifierLecteur(numero)
2. identifierLivre(isbn)
3. confirmerEmprunt()
Classes disponibles : ControleurEmprunt, Lecteur, Livre, Emprunt
Voir le corrigé
Les trois sont reçues par ControleurEmprunt. C'est le
point le plus important du corrigé : une opération système arrive de l'extérieur,
et l'extérieur ne connaît qu'une seule porte d'entrée. Répartir les trois sur trois
classes différentes reviendrait à donner trois portes au système, ce que le DSS
interdisait déjà en n'ayant qu'un seul rectangle.
1 — identifierLecteur(numero) appelle
Lecteur.estEnRegle(). Le contrôleur ne sait pas ce qu'est « être en
règle » — c'est une affaire de lecteur, et le lecteur seul connaît son amende.
2 — identifierLivre(isbn) appelle
Livre.estDisponible(). Même raisonnement.
3 — confirmerEmprunt() crée un Emprunt et
appelle par exemple Livre.emprunter() pour le marquer sorti. C'est la
seule des trois qui change quelque chose, et c'est pour ça qu'elle porte le
nom d'une confirmation.
Remarque au passage ce que le corrigé vient de faire : il a distribué des responsabilités en se demandant chaque fois qui possède l'information nécessaire. Ce raisonnement a un nom et sa propre semaine de cours — Les principes GRASP. Tu l'as employé ici sans le savoir.
La navigabilité, ou le trait qui devient un attribut
Dans le modèle du domaine, une association est un fait : « un emprunt concerne un livre ». Le fait n'a pas de direction — il est vrai des deux côtés à la fois. En conception, ce confort disparaît, parce qu'un objet en mémoire ne connaît que ce qu'il garde. Il faut décider qui garde la référence de qui, et cette décision s'appelle la navigabilité.
Elle se note par une flèche ouverte à l'extrémité qu'on peut atteindre. Et il faut connaître la convention du cours avant tout le reste, parce qu'elle est contre-intuitive et qu'elle tombe en examen : une association sans aucune flèche se lit comme bidirectionnelle — chacun peut atteindre l'autre. Ne pas trancher n'est donc pas neutre : c'est choisir par défaut l'option la plus coûteuse.
Emprunt porte livre et Livre porte une collection d'emprunts, à tenir d'accordLivre — un seul attribut, livre : Livre, parce que l'extrémité visée porte 1 ; Livre ignore ses empruntsEmprunt — l'extrémité visée porte 0..*, donc Livre porte une collection, et c'est l'emprunt qui ignore son livreLe code rend cette traduction évidente, et c'est la raison pour laquelle ce module montre du Java alors qu'il parle de diagrammes : la flèche est l'attribut.
public class Emprunt {
private Date datePret;
private Date dateRetourPrevue;
// LA FLÈCHE « Emprunt ──▶ Livre » EST CETTE LIGNE.
// Multiplicité 1 du côté Livre : un seul champ, pas une collection.
private Livre livre;
// Rien dans Livre ne pointe vers Emprunt : la flèche ne va que
// dans un sens, donc Livre ignore par quels emprunts il est passé.
public boolean estEnRetard(Date aujourdhui) {
return aujourdhui.after(dateRetourPrevue);
}
}
Une association navigable donne un champ dont le type est la classe visée. C'est la seule chose que la flèche veut dire — et c'est aussi pourquoi une flèche tracée à la légère devient une dépendance qu'on traîne jusqu'au dernier livrable.
La troisième ligne de la figure mérite qu'on s'y arrête, parce qu'elle est le cas que le
code rate le plus souvent. Si l'extrémité visée porte 0..* ou
1..*, l'attribut n'est plus une référence unique :
c'est une collection. Une Commande navigable vers ses
LigneCommande en multiplicité 1..* donne
private List<LigneCommande> lignes; — et sur le diagramme, on a le
droit d'écrire la même chose dans le compartiment des attributs,
- lignes : LigneCommande [1..*]. Les deux notations disent exactement la
même chose ; on choisit le trait quand la classe visée est importante, l'attribut
quand elle est secondaire.
Comment décide-t-on du sens ? Par les besoins réels, jamais par symétrie. La question à poser est : y a-t-il un scénario où un objet de cette classe doit envoyer un message à l'autre ? Si oui, il faut la flèche ; si non, elle ne sert à rien et coûte quelque chose. Trois indices classiques qu'une flèche est nécessaire : A crée B, A doit lire ou modifier B, ou A reçoit un message qui l'y oblige.
Mettre des flèches dans les deux sens par réflexe. Ça paraît généreux — « comme ça, tout le monde peut tout atteindre » — et c'est la décision la plus coûteuse du diagramme.
Chaque association bidirectionnelle crée deux attributs qui doivent rester d'accord. Retirer une ligne d'une commande demande alors de la retirer de la collection et d'effacer le champ qui remonte, sinon la mémoire contient deux vérités contradictoires. Elle crée aussi un cycle : les deux classes dépendent l'une de l'autre, on ne peut plus en compiler, en tester ni en réutiliser une sans l'autre. La règle pratique est la même que pour les losanges du modèle du domaine : en cas de doute, une seule flèche.
Ce code prétend réaliser l'association Commande 1 ——▶ 0..*
LigneCommande. Qu'est-ce qui ne correspond pas au diagramme ?
public class Commande {
private LigneCommande ligne;
private Client client;
}
public class LigneCommande {
private Commande commande;
}
Deux écarts, et le second est le plus grave.
La multiplicité n'est pas respectée. Le diagramme annonce
0..* du côté LigneCommande, donc une collection —
private List<LigneCommande> lignes;. Le champ unique
ligne ne peut en tenir qu'une : le code réalise en fait un
0..1.
Une flèche a été ajoutée sans être au diagramme.
LigneCommande porte un champ commande, ce qui rend
l'association bidirectionnelle alors qu'elle ne l'était pas. C'est exactement le
piège ci-dessus, arrivé par le code au lieu du dessin — et c'est ainsi qu'il arrive
le plus souvent.
Le champ client, lui, n'est pas une faute : il réalise une
autre association, qui a simplement le droit d'exister. Un diagramme ne
montre jamais qu'une paire de classes à la fois.
La dépendance, et ce qu'un pointillé avoue
Il existe un quatrième trait, qu'on n'a pas encore croisé parce qu'il n'a aucun sens en analyse : la dépendance. Elle se dessine par une flèche en pointillé, et elle dit quelque chose de plus faible qu'une association : « cette classe a besoin de connaître l'autre, mais elle ne la garde pas ».
Le critère est la durée. Une association vit aussi longtemps que l'objet : le champ est là, entre deux appels, tant que l'objet existe. Une dépendance ne vit que le temps d'un appel de méthode. Trois formes, et ce sont les trois que l'examen montre :
- la classe reçoit l'autre en paramètre d'une de ses opérations ;
- elle la retourne ;
- elle l'utilise localement, ou appelle une de ses opérations de
classe (une méthode
static).
Emprunt garde un champ livre, disponible entre deux appels et tant que l'emprunt existeControleurEmprunt reçoit un livre en paramètre, s'en sert, et n'en garde aucune trace après l'appelLivre, et ce qui les sépare est la durée du
lien : le trait plein donne un attribut permanent, le trait pointillé un usage
passager. Noter aussi qu'une dépendance ne porte pas de
multiplicités — il n'y a rien à compter dans un lien qui ne dure pas.
Pourquoi consacrer une section à un trait aussi discret ? Parce qu'il est le lien le plus faible avec lequel deux classes peuvent encore collaborer, et que c'est exactement ce qu'on cherche. Deux classes reliées par une association sont soudées : on ne peut plus toucher à l'une sans risquer l'autre. Deux classes reliées par une dépendance ne se rencontrent que le temps d'un appel.
D'où une habitude qui vaut pour tout le reste de la session : quand une
association peut se remplacer par une dépendance, on le fait. Si le contrôleur
n'a besoin du livre que pendant confirmerEmprunt(), il ne doit pas le
stocker ; il le reçoit, l'utilise, l'oublie. Ce réflexe porte un nom qu'on te
donnera formellement en semaine 6 — le faible couplage, l'un des
Les principes GRASP —, et tu peux commencer à l'appliquer avant de connaître son nom.
Les classes que le métier n'a jamais eues
Le modèle du domaine ne contenait que des concepts prononçables par le client. Le DCC en
contient d'autres, et ces autres sont souvent la majorité des boîtes. Le client ne
reconnaîtrait ni ControleurEmprunt, ni FenetreEmprunt, ni
EmpruntDAO — et c'est très bien, parce que ce sont des réponses à des
questions qu'il ne se pose pas.
Pour les ranger, UML permet de coller une étiquette sur une classe : un stéréotype, écrit entre chevrons doubles. Trois d'entre eux forment le vocabulaire de base de la conception orientée objet, hérités de l'analyse dite « robuste » d'Ivar Jacobson.
| Stéréotype | Son rôle | Exemples |
|---|---|---|
| «entity» entité |
porter un concept du métier et ses données : c'est ce qui survit à l'arrêt du programme | Emprunt, Livre, Lecteur |
| «boundary» frontière |
tout ce qui touche au monde extérieur — un acteur humain, mais aussi un autre système ou un appareil | FenetreEmprunt, PasserellePaiement |
| «controller» contrôleur |
recevoir une opération système et orchestrer les entités qui la réalisent ; il coordonne, il ne calcule pas | ControleurEmprunt |
Le contrôleur mérite qu'on s'y arrête, parce que c'est la classe qui
répond à la question laissée ouverte plus haut : où atterrit une opération
système ? Elle atterrit sur un contrôleur, et il y a deux façons d'en choisir
un. Soit un contrôleur par cas d'utilisation —
ControleurEmprunt, ControleurRetour —, soit un contrôleur unique
qui représente le système entier ou l'organisation. Le premier choix est le plus sûr dès
que le système a plus d'une poignée de cas d'utilisation.
La frontière est plus large que ce que son nom courant laisse croire : ce n'est pas seulement l'interface graphique. Une classe qui parle à une passerelle de paiement est une frontière, une classe qui lit un lecteur de codes-barres aussi. C'est la même idée que les acteurs non humains du module Cas d'utilisation & DSS, descendue d'un niveau.
Et il en manque une quatrième dans le trio classique, que tout projet réel possède :
la persistance. Les classes en DAO (data access
object) ou en Repository savent aller chercher une entité dans une base
de données et l'y remettre. On les traite en pratique comme une frontière — la base est
un système externe comme un autre.
Si ce trio te rappelle quelque chose, c'est normal : MVC — Modèle-Vue-Contrôleur — est l'application de ces trois rôles aux interfaces graphiques. La vue est une frontière, le modèle une entité, et le contrôleur porte le même nom des deux côtés. MVC n'est donc pas une notion de plus à apprendre : c'est le vocabulaire que le monde du logiciel a retenu de ces stéréotypes.
Prendre un stéréotype pour un type du langage. «entity», «boundary» et «controller» ne s'écrivent nulle part en Java. Ce ne sont ni des classes à hériter, ni des interfaces à implémenter, ni des annotations : ce sont des rôles, c'est-à-dire des étiquettes que tu poses sur le diagramme pour dire ce que la classe est venue faire.
Ils guident la conception, et ils s'évaporent au moment du code — sauf si tu emploies un cadriciel qui les matérialise, ce qui n'est pas le cas ici. La marque qu'il en reste dans un projet Java est ailleurs, et elle est plus utile : c'est le paquetage dans lequel la classe se range, sujet de la section suivante.
Attribue un stéréotype à chacune de ces cinq classes, et repère celle qui pose un problème de conception.
(A) FormulaireConnexion
(B) Lecteur
(C) ControleurRetour
(D) LecteurCodeBarres
(E) ControleurPrincipal
+ emprunter() + retourner() + inscrire()
+ payerAmende() + reserver() + imprimerRapport()
(A) frontière — c'est de l'interface graphique. (B) entité — un concept du domaine, il était déjà au modèle du domaine. (C) contrôleur. (D) frontière, et c'est le piège de vocabulaire du lot : le mot « lecteur » désigne ici un appareil, pas une personne. Deux classes qui commencent par le même mot n'ont aucune raison d'être de la même famille.
(E) est le problème. C'est bien un contrôleur, mais il en fait trop : six opérations système qui viennent de six cas d'utilisation différents. Le symptôme se reconnaît sans réfléchir — un contrôleur dont la liste d'opérations ressemble au sommaire du système. Il devient le point par lequel tout passe, donc celui qu'on modifie à chaque changement.
Le remède est de le découper par cas d'utilisation : ControleurEmprunt,
ControleurRetour, ControleurInscription. Ce défaut a un nom
officiel et une place dans le cours — le contrôleur surchargé, en semaine 6 avec
Les principes GRASP.
L'architecture logique : regrouper en paquetages
Une trentaine de classes tiennent encore sur une feuille. Deux cents, non — et le vrai problème n'est même pas le dessin : c'est qu'on ne sait plus dire ce qui dépend de quoi. L'architecture logique répond à ça. Elle est l'organisation des classes en paquetages et les dépendances entre ces paquetages, et c'est la seconde moitié de ton Livrable 2.
Le mot est resté sans définition jusqu'ici, alors même qu'il apparaissait dans le tableau de visibilité de la formule. Un paquetage est un regroupement nommé de classes — rien de plus, et c'est déjà beaucoup : il crée une frontière, donc un intérieur et un extérieur. Sa notation UML est un rectangle avec un onglet en haut à gauche, comme une chemise cartonnée, et les dépendances entre paquetages se tracent en pointillé, exactement comme entre classes.
En Java, la correspondance est directe et c'est ce qui la rend facile à retenir :
un paquetage est un dossier, et chaque fichier annonce le sien par une
ligne package. L'arborescence de ton dépôt est ton architecture
logique.
src/
└── bibliotheque/
├── presentation/ FenetreEmprunt, FenetreRetour
├── application/ ControleurEmprunt, ControleurRetour
├── domaine/ Emprunt, Livre, Lecteur
└── persistance/ EmpruntDAO, LivreDAO
Quatre paquetages, quatre dossiers, et la même chose vue de deux façons. Le diagramme de paquetages du livrable ne dit rien de plus que cette arborescence, plus les flèches qui disent qui a le droit d'appeler qui.
Le ~ de la formule prend enfin son sens : une visibilité
paquetage rend un membre accessible aux classes du même paquetage et à personne
d'autre. En Java, c'est ce qu'on obtient en n'écrivant aucun mot-clé devant un membre — le
défaut du langage. C'est utile pour laisser deux classes voisines collaborer étroitement
sans ouvrir la porte au reste du système.
Reste la question qui décide de tout : selon quoi découpe-t-on ? Deux réponses s'affrontent dans la vraie vie. Par couche technique — présentation, application, domaine, persistance — ou par fonctionnalité — emprunt, retour, inscription, chacune contenant sa fenêtre, son contrôleur et ses entités. Le cours, et Larman avec lui, enseignent le découpage par couche : c'est celui que ton livrable doit montrer, et celui que la section suivante explique.
Une seule règle est absolue, quel que soit le découpage : pas de cycle entre
paquetages. Si presentation dépend de domaine et que
domaine dépend de presentation, les deux ne forment plus qu'un
seul bloc qu'on ne peut ni compiler, ni tester, ni comprendre séparément — et on a perdu
exactement ce qu'on était venu chercher.
Le module Cas d'utilisation & DSS t'a fait traiter le système comme une boîte noire : on déclare ce qui franchit sa frontière, et ce qu'il y a derrière reste libre de changer. Un paquetage est cette même idée appliquée à l'intérieur du système.
Ses classes publiques sont ses opérations système ; le ~ et le
- sont ce qui reste dans la boîte. Et le bénéfice est identique : ce
qui est caché peut changer sans prévenir personne. Tu retrouveras cette idée sous
d'autres noms toute ta vie — l'interface d'un module, l'API d'un service, l'en-tête
d'une bibliothèque. C'est chaque fois la même : publier peu, se réserver le
reste.
Les couches, et la règle qui les tient
Le découpage en couches range les paquetages en strates horizontales, de l'écran vers le disque. Quatre suffisent à décrire presque tous les systèmes de gestion, et ce sont celles que ton livrable doit nommer.
| La couche | Sa responsabilité | Ses classes |
|---|---|---|
| Présentation | afficher, et capter les gestes de l'utilisateur ; elle ne décide rien | FenetreEmprunt, FormulaireConnexion |
| Application | recevoir les opérations système et orchestrer ; elle ne calcule rien elle-même | ControleurEmprunt |
| Métier | porter les concepts et les règles du domaine ; c'est la couche qui contient la valeur du logiciel | Emprunt, Livre, Lecteur |
| Persistance | ranger et retrouver les entités dans une base ou des fichiers | EmpruntDAO, LivreDAO |
La règle qui les tient tient en une phrase : une couche n'appelle que celle qui est immédiatement sous elle. Pas au-dessus, et pas en sautant. Le corollaire le plus utile en pratique : une fenêtre ne parle jamais directement à une entité. Elle s'adresse au contrôleur, qui s'adresse aux entités — c'est ce qui permet à la même règle de tourner ensuite derrière une autre interface.
Les manuels justifient cette règle par quatre mots — découplage, testabilité, réutilisation, clarté — qui sont vrais et qui n'expliquent rien. La vraie raison est plus simple, et une fois vue elle ne se réoublie pas : appeler, c'est dépendre. Si A appelle B, alors tout changement de B peut casser A. Le sens des flèches est donc le sens dans lequel les changements se propagent.
Or les quatre couches ne changent pas à la même vitesse. L'écran change tout le temps — un client vous demandera trois refontes. La base change souvent. Les règles du métier, elles, changent rarement : une bibliothèque prête des livres pour trois semaines depuis toujours. On oriente donc les dépendances de l'instable vers le stable, jamais l'inverse. Une couche métier qui connaît la fenêtre est une règle durable rendue fragile par un écran qui ne durera pas six mois.
Le test qui vérifie tout ça en une question : peux-tu remplacer l'interface graphique par une ligne de commande sans toucher au métier ? Si oui, l'architecture tient. Si non, quelqu'un a tracé une flèche vers le haut.
Le message d'erreur affiché depuis la couche métier. C'est la violation la plus fréquente, et la plus innocente en apparence : une classe du domaine détecte un problème et ouvre une boîte de dialogue pour le dire. Une ligne, et la flèche remonte de deux couches.
Ce qu'on vient de perdre : la classe ne peut plus tourner sans interface graphique, donc plus être testée automatiquement, plus servir dans un traitement de nuit, plus être appelée depuis une application web. Le bon geste est de renvoyer l'information — une exception, un booléen, un objet de résultat — et de laisser la couche du dessus décider comment l'annoncer. Une couche basse répond ; elle ne parle jamais à l'utilisateur.
Place chaque classe dans sa couche, puis dis lequel des trois appels est illégal.
Classes : FenetreCommande ServiceCalculTaxe
ControleurPaiement CommandeRepository
Commande PasserelleStripe
Appels : 1. ControleurPaiement → Commande
2. Commande → CommandeRepository
3. ControleurPaiement → CommandeRepository
Voir le corrigé
Les couches. Présentation :
FenetreCommande. Application :
ControleurPaiement. Métier : Commande et
ServiceCalculTaxe — le second porte une règle du domaine et aucune
interface, il reste donc au métier malgré son nom en « service ».
Persistance : CommandeRepository. Et
PasserelleStripe est une frontière vers un système
externe ; beaucoup d'équipes lui donnent sa propre couche, souvent nommée
infrastructure ou intégration, précisément pour pouvoir la remplacer.
L'appel illégal est le 2. Commande est une entité du
métier ; si elle appelle son repository, elle sait qu'elle est stockée
et où. La règle métier devient alors inséparable de la base de données : on ne
peut plus la tester sans en lever une, ni changer de mode de stockage sans rouvrir le
domaine.
Les deux autres descendent bien d'un cran. Le 3 mérite un mot : un contrôleur d'application qui parle à la persistance est parfaitement normal — c'est même son travail, aller chercher les entités et les remettre. Ce que la règle interdit, c'est que le métier le fasse.
Relire son diagramme avant de le rendre
Tout ce qui précède se perd si le diagramme rendu oublie ses visibilités. Cette dernière section n'apporte donc aucune notion neuve : elle donne la relecture à faire, dans l'ordre, avant de remettre une copie ou un livrable. Elle prend trois minutes et elle rapporte plus que n'importe quel raffinement de conception.
- Chaque boîte a-t-elle ses trois compartiments ? Et le troisième est-il rempli ? Une classe sans opération, dans un diagramme de conception, est soit une erreur, soit un aveu — on n'a pas encore décidé ce qu'elle fait.
- Chaque attribut et chaque opération porte-t-il sa visibilité ? C'est le premier point que le correcteur balaie, parce qu'il se vérifie sans lire.
-
Chaque attribut et chaque opération porte-t-il son type ? Retour
compris —
: voids'écrit. - Chaque association porte-t-elle ses deux multiplicités ? Le module Le modèle du domaine l'appelait déjà l'erreur la plus fréquente du cours ; elle ne disparaît pas en conception, elle s'y ajoute la navigabilité.
- Chaque flèche correspond-elle à un besoin que tu peux nommer ? Et les bidirectionnelles, les as-tu vraiment voulues ?
- Chaque classe est-elle dans une couche, et chaque dépendance descend-elle ? Une seule flèche qui remonte suffit à casser l'architecture qu'on annonce.
- Les noms viennent-ils du modèle du domaine ? Si une entité s'est fait renommer en chemin, remets son nom d'origine.
Une dernière chose, qui n'est pas de la relecture mais qui vaut d'être dite ici. Un diagramme de conception n'a pas à être complet pour être bon. Montrer trois classes et leurs vraies relations vaut mieux que trente boîtes dont on n'a rempli aucun troisième compartiment. Ce qu'on évalue est la cohérence des décisions, pas le nombre de rectangles.
Cette matière est celle de la semaine 4, évaluée à l'examen intra du 24 octobre — la révision de la semaine 7 porte sur les modules 1 à 5 du cours, dont celui-ci. Et c'est la matière du Livrable 2 — Modèle de conception et Architecture logique, à remettre le 13 octobre (10 %) : le diagramme de classes et l'architecture logique en sont les deux moitiés nommées. Chez Larman, ce sont les chapitres 12, 13, 16, 34 et 35.
IFT-2007 n'autorise aucun matériel. La grammaire d'une ligne, les quatre symboles de visibilité, la différence entre trait plein et trait pointillé et l'ordre des quatre couches doivent être en tête — ce sont quatre choses qu'on peut rater alors même qu'on a compris toute la leçon.
« Transformez ce modèle du domaine en diagramme de classes de conception. »
L'énoncé classique, et il se traite dans un ordre qui rapporte des points même quand le résultat diffère du corrigé. Reprends les entités du domaine avec leurs associations ; ajoute à chaque attribut son type et sa visibilité — privée par défaut ; ajoute les opérations que chaque entité peut porter seule ; ajoute un contrôleur qui reçoit les opérations système ; puis tranche la navigabilité de chaque association.
Ce qui distingue une bonne copie : elle justifie deux ou trois choix en une ligne — pourquoi cette flèche va dans ce sens, pourquoi cette opération est sur cette classe. Un diagramme de conception se défend ; le correcteur cherche la trace de la décision.
« Quelle est la différence entre une association et une dépendance ? »
La durée du lien, et c'est la réponse en une phrase : une association donne un attribut qui vit aussi longtemps que l'objet, une dépendance ne dure que le temps d'un appel. Puis la notation : trait plein contre trait pointillé, et une dépendance ne porte pas de multiplicités.
Ajoute la conséquence, qui montre que tu as compris plutôt que récité : la dépendance est le lien le plus faible avec lequel deux classes peuvent encore collaborer, donc celui qu'on préfère chaque fois qu'il suffit.
« Décrivez l'architecture en couches et justifiez sa règle de dépendance. »
Nomme les quatre couches dans l'ordre — présentation, application, métier, persistance —, donne un exemple de classe pour chacune, et énonce la règle : une couche n'appelle que celle immédiatement en dessous, jamais au-dessus, jamais en sautant.
Pour la justification, évite la liste de mots (découplage, testabilité…) que tout le monde écrira. Dis le mécanisme : appeler, c'est dépendre, donc le sens des flèches est le sens dans lequel un changement se propage — et on fait pointer les couches instables vers les couches stables, jamais l'inverse. Le test de l'interface remplaçable par une ligne de commande vaut un point à lui seul.
Le diagramme de classes de conception emploie la notation du modèle du domaine pour dire tout autre chose : non plus ce que le métier est, mais ce que le logiciel fera. Ce qu'il ajoute — les types, la visibilité, les opérations, les classes techniques — n'est jamais une simple précision : chaque ligne est une décision qu'on aurait pu prendre autrement, et c'est pour ça qu'un tel diagramme ne se fait pas valider par le client, il se défend devant l'équipe.
Deux traits neufs font tout le travail. La navigabilité décide qui garde la référence de qui, et se traduit littéralement en champs : une flèche, un attribut ; une extrémité à plusieurs, une collection. La dépendance, en pointillé, dit qu'on se sert d'une classe sans la garder — c'est le lien le plus faible avec lequel deux classes collaborent encore, donc celui qu'on préfère. Au-dessus des classes, l'architecture logique range le tout en paquetages et en couches, et une seule règle les tient : on n'appelle que la couche du dessous, parce qu'appeler, c'est dépendre.
Et ailleurs : le raisonnement des couches n'a rien de propre à UML ni à Java. C'est la même question partout — qui a le droit de connaître qui ? — et elle décide de la santé d'un projet bien plus sûrement que le choix d'un langage. Tu la retrouveras dans un découpage en modules, dans les frontières d'un service web, dans un projet frontend où l'accès aux données remonte jusque dans les composants d'affichage. Savoir tracer une flèche dans le bon sens, c'est savoir quelle partie d'un système pourra changer sans emporter le reste.
- Les quatre symboles de visibilité
+public ·-privé ·#protégé (la classe et ses sous-classes) ·~paquetage- La forme d'une ligne d'attribut
visibilité nom : Type [multiplicité] = valeurParDefaut— seuls le nom et la visibilité sont obligatoires- La forme d'une ligne d'opération
visibilité nom(param : Type, ...) : TypeRetour— le type de retour s'écrit toujours,voidcompris- Ce que veulent dire le soulignement et l'italique
- souligné = membre de classe (
static) · italique = abstrait, qu'il s'agisse d'une classe ou d'une opération - Ce que devient une flèche de navigabilité dans le code
- un attribut du type visé. Multiplicité
1→ un champ ;0..*ou1..*→ une collection - Ce qui distingue un trait plein d'un trait pointillé
- la durée : l'association garde un attribut, la dépendance ne dure qu'un appel — paramètre, retour, variable locale. Une dépendance ne porte pas de multiplicités
- Où atterrit une opération système
- sur un contrôleur, un par cas d'utilisation. C'est la seule porte d'entrée, comme le DSS n'avait qu'un seul rectangle
- Les trois stéréotypes, et ce que chacun touche
- «entity» le domaine · «boundary» le monde extérieur (écran, appareil, autre système) · «controller» l'orchestration
- Les quatre couches, dans l'ordre
- présentation, application, métier, persistance — et une couche n'appelle que celle immédiatement en dessous
- Ce qu'un paquetage vaut en Java
- un dossier, plus une ligne
package. Sans mot-clé devant un membre, la visibilité est celle du paquetage - Le premier point que le correcteur vérifie
- la visibilité de chaque attribut et de chaque opération : elle se contrôle sans rien lire, et son absence se répète sur toute la copie