Les diagrammes d'interaction
Le DSS s'arrêtait à la porte du système : une flèche entrait, une réponse sortait, et ce qu'il y avait derrière restait volontairement noir. Ce module ouvre la boîte. On va regarder des objets se parler pour réaliser une seule opération, dans l'ordre exact où ils le font — et récolter, en sortant, les opérations que ton diagramme de classes attend encore.
Tous les diagrammes vus jusqu'ici décrivaient des choses : des concepts, des cas d'utilisation, des classes. Celui-ci décrit un déroulement — une exécution, message par message, comme si tu mettais un logiciel au ralenti pour voir qui parle à qui.
Et il a une particularité qu'aucun autre n'a : son vrai produit n'est pas le dessin. Ce qu'on garde d'un diagramme d'interaction, c'est la liste des messages qu'il contient, parce que chacun devient une opération dans une boîte du diagramme de classes. On le dessine pour décider, pas pour l'encadrer.
Ce que le DSS laissait en blanc
Reprends le diagramme de séquence système de l'emprunt, celui du module
Cas d'utilisation & DSS. Il
contenait un acteur, un rectangle nommé Système, et trois flèches
entrantes :
Usager ──▶ Système
identifierLecteur(numero)
Usager ──▶ Système
identifierLivre(isbn)
Usager ──▶ Système
confirmerEmprunt()
Les trois opérations système de l'emprunt, telles que la section Les opérations système les a relevées : une par flèche entrante, et rien de plus.
Ce diagramme dit ce que le système doit savoir faire. Il ne dit pas un mot de la façon dont il s'y prend, et c'était le but : à ce stade, on n'avait rien décidé. Le moment est venu de décider. Un diagramme d'interaction montre les objets qui collaborent pour réaliser UNE opération système, et les messages qu'ils s'échangent.
Retiens le mot une. On ne dessine pas « le diagramme d'interaction du
système » — ça n'existe pas. On en dessine un par opération système : un pour
identifierLecteur, un pour identifierLivre, un pour
confirmerEmprunt. Trois flèches entrantes au DSS, trois diagrammes. C'est
aussi mécanique que ça, et c'est la première chose qu'un correcteur vérifie : le
message qui entre dans ton diagramme est-il bien une opération système ?
Le DSS est le programme affiché à l'entrée de la salle : on y lit que la soirée comporte trois pièces, et dans quel ordre. Il ne dit rien de ce que fait le deuxième violon à la mesure 47.
Le diagramme d'interaction est la partition d'une pièce : qui joue, quand, et quoi. Soixante musiciens sans partition, c'est une cacophonie — trente classes qui se rendent service sans plan d'appel, c'est la même chose, sauf qu'on ne s'en aperçoit qu'au moment où il faut modifier quelque chose.
UML offre deux notations pour cette même partition. Le diagramme de séquence range les messages sur un axe du temps qui descend ; le diagramme de communication les pose sur un réseau d'objets, et les numérote. Le module Le processus unifié & UML l'a déjà annoncé, et il faut le prendre au pied de la lettre : les deux contiennent exactement la même information. Ce module apprend d'abord la séquence, parce que c'est celle que le cours dessine le plus, puis montre comment la même chose s'écrit en communication.
Les pièces d'un diagramme de séquence
Un diagramme de séquence n'a que quatre pièces, et tout le reste du module n'est que
leurs combinaisons. Le vocabulaire compte ici plus qu'ailleurs : un correcteur qui
lit « la ligne de vie de :Livre s'arrête après le retour » sait
immédiatement que la notion n'est pas acquise.
La ligne de vie est l'objet, vu sur toute la durée du scénario : une boîte en haut de la page, et sous elle un long trait pointillé qui descend. Ce trait est le temps. Rien n'y est à l'échelle — deux messages séparés d'un centimètre ne durent pas plus longtemps qu'à un millimètre : la seule chose que la verticale affirme, c'est l'ordre.
Le contenu de la boîte suit une règle qui tombe souvent, et qui tient au
deux-points. :Livre se lit « un objet quelconque de la
classe Livre » : le deux-points n'est pas de la ponctuation, c'est
lui qui dit « instance de ». Quand il faut distinguer deux objets de la même
classe, on les nomme : emprunte:Livre. Et Livre tout court,
sans deux-points, ne désigne pas un objet mais la classe elle-même — on ne s'en
sert que pour lui envoyer une opération de classe.
Le message est une flèche horizontale qui va de l'émetteur au destinataire et porte le nom de l'opération demandée, avec ses arguments. Le retour est ce que le destinataire renvoie. Et la barre d'activation — un rectangle étroit posé sur la ligne de vie — dit que cet objet est en train de travailler.
Cette dernière pièce est celle qu'on lit mal, alors elle mérite sa phrase : le trait pointillé dit que l'objet existe, la barre dit qu'il exécute. Un objet passe toute la page sur le diagramme ; il ne travaille que par intervalles, et chacun commence quand il reçoit un message, se termine quand il a fini de répondre.
Boîte, trait, barre, message
LIGNE DE VIE
:Livre un objet de Livre
emprunte:Livre un objet nommé
Livre la CLASSE, pas un objet
Usager un acteur, sans boîte
sous la boîte, un trait POINTILLÉ :
c'est le TEMPS. Rien n'est à l'échelle,
seul l'ORDRE compte.
BARRE D'ACTIVATION
posée sur le trait = l'objet EXÉCUTE
début : la réception ; fin : la réponse
MESSAGE
[garde] valeur := nom(arg : Type)
estDisponible()
[en regle] no := emprunter(livre)
RETOUR
flèche pointillée, portant la VALEUR
La grammaire d'un diagramme de séquence, en entier. On la reconnaît à ce que tout y est facultatif sauf le nom du message : la garde, la valeur de retour et même les arguments s'omettent dès qu'ils vont de soi.
Voici les messages d'un diagramme de séquence, dans l'ordre. Sur la
ligne de vie de :Livre, où commence et où finit la barre
d'activation ?
1 Usager ──▶ :ControleurEmprunt
confirmerEmprunt()
2 :ControleurEmprunt ──▶ :Livre
estDisponible()
3 :Livre - -▶ :ControleurEmprunt
vrai
4 :ControleurEmprunt ──▶ :Emprunt
«create»
5 :ControleurEmprunt - -▶ Usager
dateRetour
Elle commence à la ligne 2, quand :Livre reçoit
estDisponible(), et se termine à la ligne 3, quand il a
rendu sa réponse. Elle ne couvre ni la ligne 4, ni la ligne 5.
Ce que la question teste : :Livre existe pendant tout le
diagramme — son trait pointillé descend du haut en bas, comme celui des autres —,
mais il ne travaille que sur un aller-retour. Deux informations différentes,
deux traits différents.
Regarde au passage la barre du contrôleur : elle commence à la ligne 1 et ne s'arrête qu'à la ligne 5. C'est normal, et c'est même le signe qu'on a compris — tant qu'un objet attend la réponse d'un autre, il est toujours en train d'exécuter. Les barres s'emboîtent comme des poupées russes.
Tracer la barre d'activation sur toute la hauteur. C'est le réflexe du débutant pressé : on épaissit la ligne de vie du haut en bas, sur tous les objets, et le diagramme redit ce qu'il disait sans barres — on ne sait plus qui a la main.
La faute inverse est plus discrète : arrêter la barre de l'appelant pendant
qu'il attend. Si :ControleurEmprunt a envoyé
estDisponible() et attend la réponse, il n'a pas fini son travail : sa
barre continue, par-dessous celle de :Livre. Une barre ne s'interrompt
qu'au moment où son objet rend la main pour de bon.
Les trois traits, et ce que chacun décide
Toutes les flèches d'un diagramme de séquence ne se valent pas, et ce qui les distingue tient à deux détails de tracé : le trait est plein ou pointillé, la pointe est un triangle plein ou deux simples traits. Trois combinaisons servent, et chacune dit quelque chose de précis.
Le message synchrone — trait plein, pointe pleine — est celui que tu
traceras presque toujours. Il veut dire : l'émetteur s'arrête et attend la
réponse. C'est exactement ce que fait un appel de méthode ordinaire : quand tu
écris livre.estDisponible() en Java, la ligne suivante ne s'exécute pas tant
que la méthode n'a pas rendu sa valeur.
Le message asynchrone — trait plein, pointe fine — dit l'inverse : l'émetteur poste son message et continue immédiatement. On s'en sert pour un traitement lancé en arrière-plan, une notification, un autre fil d'exécution. Dans un projet en Java comme celui de la session, il est rare ; il faut savoir le lire, et savoir qu'on ne le trace pas par défaut.
Le retour — trait pointillé, pointe fine — remonte vers l'appelant et
porte une valeur, jamais un nom d'opération. On écrit vrai,
dateRetour, montant ; écrire
retournerLaDate() serait un contresens — personne n'appelle rien, quelque
chose revient.
:Livre renvoie, et c'est une valeur, pas un nom d'opérationestDisponible(), et ne disent pourtant
pas la même chose : la première fait attendre le contrôleur, la seconde le laisse
filer. La troisième n'est pas un message mais ce qui revient. Le tracé suffit donc à
trancher, avant même qu'on ait lu ce qui est écrit dessus — et un message asynchrone
comme celui du milieu ne se justifierait pas ici, puisqu'on attend forcément de savoir
si le livre est disponible.
Reste une question qui revient à chaque diagramme : faut-il tracer tous les
retours ? Non, et Larman est explicite là-dessus. Un retour se note de deux
façons — la flèche pointillée, ou la forme valeur := message() écrite
directement sur la flèche d'appel — et on ne fait jamais les deux à la fois.
Quand la réponse va de soi ou ne sert à rien de la suite, on l'omet. Ce qu'on ne peut pas
omettre, c'est la valeur dont le reste du diagramme se sert : le lecteur ne saurait
alors pas d'où elle sort.
Prendre la pointe pour une coquetterie de dessinateur. On trace la même flèche partout, en supposant que l'outil choisit une apparence. Or ces deux traits pleins ne disent pas la même chose du tout : l'un bloque l'émetteur, l'autre le laisse filer.
La contradiction que ça produit se repère à l'œil nu : un message asynchrone ne peut pas avoir de retour. Si tu traces une pointe fine, puis une flèche pointillée qui revient avec une valeur, tu as écrit l'impossible — l'émetteur est déjà passé à la suite, il n'y a plus personne pour recevoir la réponse.
Naître et mourir pendant la séquence
Les objets d'un diagramme n'étaient pas tous là au début. Réaliser
confirmerEmprunt(), c'est précisément fabriquer un
Emprunt qui n'existait pas — et la notation a de quoi le montrer.
La création se note par un message portant le stéréotype
«create», tracé non pas vers une ligne de vie mais vers la
boîte de l'objet créé ; et cette boîte se dessine plus bas que
les autres, à la hauteur du message qui la fait naître. C'est encore la position
verticale qui parle : au-dessus de sa boîte, l'objet n'existe pas.
La destruction se note par un grand × qui ferme la ligne de
vie, précédé au besoin d'un message «destroy». En Java, on ne détruit rien à
la main — le ramasse-miettes s'en charge quand plus personne ne tient l'objet —, donc tu
traceras rarement ce symbole. Il faut savoir le lire, et savoir qu'il signifie
« à partir d'ici, cet objet n'est plus », pas « le scénario se
termine ».
L'auto-appel est le troisième cas particulier : un objet s'envoie un message à lui-même. La flèche part de sa ligne de vie et y revient un peu plus bas, et une seconde barre d'activation se pose sur la première, légèrement décalée vers la droite. On s'en sert quand une opération publique délègue une partie du travail à une opération de la même classe — c'est fréquent, et c'est une façon honnête de montrer qu'un objet fait deux choses distinctes.
Reste l'acteur, et il tend un piège de vocabulaire. Il se dessine en bonhomme, sans boîte et sans deux-points, il a bien une ligne de vie — mais ce n'est pas un objet du système. Il est dehors : c'est lui qui déclenche l'opération et lui qui en reçoit le résultat, jamais lui qui exécute quoi que ce soit à l'intérieur. La question d'examen « combien d'objets ce diagramme contient-il ? » se joue exactement là.
Combien d'objets ce diagramme met-il en scène ? Et où est l'auto-appel ?
1 Client ──▶ :ControleurGuichet
retirer(500)
2 :ControleurGuichet ──▶ :Compte
verifierSolde(500)
3 :Compte ──▶ :Compte
soldeDisponible()
4 :Compte - -▶ :ControleurGuichet
vrai
5 :ControleurGuichet ──▶ :Distributeur
delivrer(500)
6 :ControleurGuichet - -▶ Client
recu
Trois objets : :ControleurGuichet,
:Compte, :Distributeur. Il y a bien quatre lignes de
vie, mais Client est un acteur — il déclenche et il
reçoit, il n'exécute rien dans le système. Répondre « quatre objets » est la
faute la plus courante sur cette question, et c'est aussi ce qui distingue une lecture
rapide d'une lecture juste.
L'auto-appel est la ligne 3 : :Compte s'envoie
soldeDisponible() à lui-même. Sur le dessin, la flèche sort de sa ligne
de vie et y revient, et une deuxième barre se pose sur la sienne.
Un détail qui vaut des points : :Distributeur est une
frontière — du matériel, pas une entité du domaine. Le module
Le diagramme de
classes & l'architecture logique a posé ce vocabulaire, et un diagramme
d'interaction fait apparaître les trois stéréotypes côte à côte : le contrôleur
orchestre, l'entité sait, la frontière touche le monde extérieur.
Quand le diagramme doit dire « si » et « tant que »
Tout ce qui précède décrit une exécution où chaque message part, une fois, dans l'ordre. Un vrai scénario n'est jamais aussi docile : le livre peut être indisponible, le lecteur peut emprunter trois ouvrages d'un coup. Il faut donc pouvoir écrire un si et un tant que dans un diagramme, et UML 2 le fait avec les fragments combinés.
Un fragment est un cadre tracé autour d'une portion du diagramme. En haut à gauche du cadre, un petit onglet porte l'opérateur — le mot qui dit quelle sorte de contrôle on applique. À l'intérieur, une garde entre crochets donne la condition. Et les messages, eux, ne changent pas : c'est tout l'intérêt de la notation, on entoure ce qu'on avait déjà.
| Opérateur | Ce qu'il fait | L'équivalent en code |
|---|---|---|
| alt | plusieurs branches, séparées par un trait pointillé ; une seule s'exécute, celle dont la garde est vraie | if / else if / else |
| opt | une seule branche : elle a lieu, ou elle n'a pas lieu | if sans else |
| loop | le contenu se répète tant que la garde est vraie ; on écrit aussi
loop(1,n) pour un nombre connu de tours |
while, for |
| par | les branches se déroulent en même temps, dans n'importe quel ordre d'entrelacement | deux fils d'exécution |
| ref | renvoie à un autre diagramme au lieu de le redessiner | un appel de méthode dont on ne montre pas le corps |
Les trois premiers sont ceux qu'on trace et ceux qui tombent en examen.
par ne sert que si ton système a réellement des traitements simultanés, et
ref est un outil de mise en page : il évite de recopier dans dix
diagrammes la même sous-séquence d'authentification.
emprunter(livre) est écrit à l'identique dans les trois cadres : ce
qui change est le cadre, et lui seul. Dans le premier, le message est l'une des deux
issues possibles ; dans le deuxième, il a lieu ou n'a pas lieu ; dans le
troisième, il a lieu plusieurs fois. C'est aussi la façon la plus rapide de distinguer
alt de opt sur une copie : le premier a un trait
pointillé au milieu, le second n'en a pas.
Ce fragment est censé dire « si le solde suffit, on débite ; sinon, on prévient l'usager ». Qu'est-ce qui ne va pas ?
opt [solde >= montant]
:ControleurGuichet ──▶ :Compte
debiter()
:ControleurGuichet ──▶ Usager
erreur()
L'opérateur est faux. Un opt n'a qu'un seul
opérande : tout ce qui est dans le cadre s'exécute ensemble quand la garde est
vraie. Ici, un solde suffisant déclencherait donc le débit et le message
d'erreur, et un solde insuffisant ne ferait rien du tout — l'usager ne serait jamais
prévenu.
Il faut un alt, un trait pointillé entre les deux messages, et une garde
par branche : [solde >= montant] au-dessus du débit,
[else] au-dessus de l'erreur.
La règle qui évite la faute : compte les issues. Deux issues
possibles, deux branches, donc alt. Une seule issue qui peut ne pas
arriver, donc opt. Le mot « sinon » dans un énoncé est le
signal : il n'y a pas de « sinon » dans un opt.
Écrire la condition dans le nom du message. On voit régulièrement
emprunterSiDisponible() — et le fragment disparaît, avec la condition qu'il
portait. Le nom d'une opération dit ce qu'elle fait ; le
quand appartient à la garde, et la garde appartient au cadre.
La variante de la même faute est le cadre sans garde du tout : un
alt dont les branches ne portent aucun crochet n'affirme plus rien, sinon
qu'il existe deux possibilités quelque part. C'est la multiplicité oubliée du module
précédent, transposée : le correcteur balaie les crochets avant de lire le reste,
parce que ça se vérifie sans réfléchir.
Les mêmes messages, vus en réseau
Le diagramme de communication raconte exactement la même chose que le diagramme de séquence, avec une seule différence : il n'a pas d'axe du temps. Les objets sont posés où l'on veut sur la page, reliés par des traits nus — des liens —, et chaque message s'écrit le long du lien qu'il emprunte, avec une petite flèche pour le sens.
Une question devient alors urgente : si rien n'est en haut ni en bas, comment sait-on lequel part en premier ? Par le numéro. C'est lui, et lui seul, qui porte la chronologie. Un diagramme de communication sans numéros n'est pas incomplet, il est illisible : on voit qui connaît qui, on ne voit plus rien de ce qui se passe.
Et ce numéro dit plus que l'ordre : il dit la profondeur d'appel. Le
message qui déclenche tout porte 1. Ceux qui partent pendant son
traitement portent 1.1, 1.2, 1.3 ; si
1.2 en déclenche un autre à son tour, ce sera 1.2.1. La
numérotation est donc l'exact équivalent des barres d'activation emboîtées de la
séquence — la même information, écrite au lieu d'être dessinée.
Deux marques complètent la notation. L'astérisque signale la
répétition : 1.3*: emprunter(livre) remplace le cadre
loop. Et la garde s'écrit entre crochets avant les
deux-points : 1.3 [disponible]: emprunter(livre). Un diagramme de
communication n'a pas de cadres ; tout ce que les fragments portaient se replie dans
la ligne du message.
1.3 emprunter(livre) ▶ :Livre
:Livre porte plusieurs messages, et c'est leur numéro qui les sépare dans le tempsemprunter(livre) apparaît
au-dessus de «create» alors qu'il part après : la position ne veut
plus rien dire, le 1.3 le dit à sa place.
Voici un diagramme de communication pour le retour d'un livre, tel qu'on le relèverait en le parcourant à l'œil — dans l'ordre où les liens tombent sous les yeux, pas dans l'ordre du temps.
:ControleurRetour ▶ :Emprunt
1.2 : estEnRetard(aujourdhui)
1.4 : cloturer()
:ControleurRetour ▶ :Amende
1.3 [en retard] : calculer()
Usager ▶ :ControleurRetour
1 : retournerLivre(isbn)
:ControleurRetour ▶ :RegistreEmprunts
1.1 : trouver(isbn)
Quatre questions : (a) dans quel ordre les messages partent-ils réellement ? (b) lequel peut ne pas partir du tout, et qu'est-ce qui le dit ? (c) combien d'objets ce diagramme met-il en scène ? (d) si on redessinait la même chose en séquence, quel fragment faudrait-il, et autour de quoi ?
Voir le corrigé
(a) 1, 1.1, 1.2,
1.3, 1.4 — c'est-à-dire : l'usager rend le livre, le
contrôleur retrouve l'emprunt, lui demande s'il est en retard, fait calculer l'amende
le cas échéant, puis clôt l'emprunt. L'ordre d'affichage ne compte pour
rien : dans un diagramme de communication, la position d'un message sur
la page n'a aucune valeur. Seuls les numéros parlent.
(b) 1.3, à cause de sa garde
[en retard]. C'est le seul message conditionnel du lot : tous les
autres partent à chaque exécution.
(c) Quatre : :ControleurRetour,
:Emprunt, :Amende et :RegistreEmprunts.
Usager est un acteur, pas un objet — même piège qu'au diagramme de
séquence, et il se tend aussi bien dans les deux notations.
(d) Un opt [en retard] autour du seul message
calculer(). Pas un alt : rien n'est prévu quand le
livre est à l'heure, il n'y a donc pas de seconde branche. Remarque au passage ce que
la conversion coûte : en communication, la condition tient dans les crochets
d'une ligne ; en séquence, il faut lui dessiner un cadre.
Alors, lequel des deux dessiner ? Le cours demande de savoir lire et produire les deux, et le choix se fait sur ce qu'on veut faire ressortir. La séquence gagne dès que le scénario est long ou plein de conditions : l'ordre y saute aux yeux et les fragments s'y voient. La communication gagne quand on veut voir qui est relié à qui — et c'est loin d'être un détail esthétique : chaque lien du diagramme annonce une association ou une dépendance à venir dans le diagramme de classes.
Construire le diagramme d'une opération système
Devant une copie blanche, la notation ne sert à rien si l'on ne sait pas par où commencer.
Voici l'ordre des gestes, déroulé sur confirmerEmprunt() — la troisième
opération système du DSS de l'emprunt.
-
Le message initial est l'opération système. Il part de l'acteur et
arrive sur une seule classe : le contrôleur. Le module
Le diagramme de
classes & l'architecture logique a déjà tranché ce point — une opération système
arrive de l'extérieur, et l'extérieur ne connaît qu'une porte d'entrée. Tu n'as donc
rien à décider ici :
Usager ──▶ :ControleurEmprunt : confirmerEmprunt(). - Écris en français ce qu'il faut faire. Pour confirmer un emprunt : vérifier que le livre est disponible, créer l'emprunt, marquer le livre sorti, rendre la date de retour. Quatre choses. Cette liste ne se dessine pas encore, elle se pose à côté.
- Pour chacune, demande-toi qui possède l'information. C'est la seule question qui distribue, et elle a toujours la même forme : quel objet sait déjà ce qu'il faut savoir pour faire ça ? La disponibilité d'un livre est une affaire de livre. La date de retour est une affaire d'emprunt. Le contrôleur, lui, ne sait rien — il n'a que la liste des choses à faire.
- Chaque délégation devient un message, tracé du contrôleur vers l'objet qui sait. Et le dernier geste est le retour vers l'acteur : ce que le DSS avait promis qu'il recevrait, et que ton diagramme doit bien finir par produire.
Ce raisonnement — confier une responsabilité à qui détient l'information — porte un nom, il a sa semaine dans le cours, et tu viens de l'appliquer sans le connaître. Ce sont Les principes GRASP, la matière de la semaine 6.
Le résultat, une fois écrit en Java, montre à quel point la correspondance est serrée : un diagramme de séquence est le corps d'une méthode, dessiné. Ce bloc-ci est l'exact équivalent du diagramme qu'on vient de construire.
public class ControleurEmprunt {
private RegistreEmprunts registre;
public Date confirmerEmprunt(Lecteur lecteur, Livre livre) {
// MESSAGE 1.1 — le contrôleur ne SAIT pas si un
// livre est disponible : il le demande à celui
// qui le sait. Un appel de méthode ordinaire est
// un message SYNCHRONE — la ligne d'après attend.
if (!livre.estDisponible()) {
return null; // la branche [else] du alt
}
// MESSAGE 1.2 — «create» : l'objet naît ICI, et sa
// boîte se dessine plus bas que les autres.
Emprunt emprunt = new Emprunt(lecteur, livre);
// MESSAGE 1.3 — c'est encore le livre qui change
// son propre état.
livre.emprunter();
registre.ajouter(emprunt);
// LE RETOUR vers l'acteur : la flèche pointillée
// qui remonte tout en haut du diagramme.
return emprunt.getDateRetour();
}
}
Chaque appel de méthode est une flèche, l'accolade de la méthode est la barre d'activation
du contrôleur, et le if est le fragment alt. Une seule ligne
n'apparaîtrait pas sur le diagramme : getDateRetour(), parce que le
module précédent l'a rangée parmi les accesseurs triviaux qu'on ne trace pas — on écrit
plutôt la valeur sur la flèche de retour.
Un coéquipier propose ce diagramme pour l'opération système
payerAmende(numeroLecteur, montant). Il n'est pas faux au sens de la
notation : toutes les flèches sont bien tracées. Relève ce qui cloche quand même,
et dis pourquoi.
1 Bibliothecaire ──▶ :ControleurAmende
payerAmende(no, 5.00)
2 :ControleurAmende ──▶ :RegistreLecteurs
trouver(no)
3 :ControleurAmende ──▶ :Lecteur
getAmendeDue()
4 :ControleurAmende ──▶ :ControleurAmende
soustraire(5.00)
5 :ControleurAmende ──▶ :Lecteur
setAmendeDue(reste)
6 :ControleurAmende - -▶ Bibliothecaire
reste
Voir le corrigé
Le contrôleur fait le calcul lui-même — lignes 3, 4 et 5. Il lit le
montant dû du lecteur, le soustrait dans son coin, puis réécrit le résultat dans le
lecteur. Le :Lecteur n'est plus qu'un tiroir : on lui prend une
valeur, on lui en repose une autre.
La question qui tranche est celle de l'étape 3 de la méthode : qui possède
l'information ? Le montant dû appartient au lecteur. C'est donc lui qui doit
savoir encaisser un paiement. Les trois messages se remplacent par
un seul : :ControleurAmende ──▶ :Lecteur : payer(5.00),
et le lecteur renvoie ce qui reste dû.
Le symptôme se reconnaît sans réfléchir, et il vaut pour tous tes diagrammes :
une paire get… puis set… sur le même objet, avec un
calcul entre les deux. Chaque fois que tu la vois, le calcul est du mauvais
côté de la flèche.
Deux détails à ne pas signaler comme des fautes, en revanche. L'auto-appel de la
ligne 4 est parfaitement légal en notation — c'est ce qu'il calcule qui ne
va pas, pas le fait de s'appeler soi-même. Et :RegistreLecteurs à la
ligne 2 est un bon réflexe : on ne demande pas à la classe Lecteur
de retrouver un lecteur, on le demande à l'objet dont c'est le métier de tenir la
liste.
Le contrôleur qui fait tout. C'est la faute numéro un des premiers diagrammes d'interaction, et elle se voit à la forme du dessin : toutes les flèches partent de la même ligne de vie, aucune ne va d'une entité à une autre. On appelle ça un diagramme en peigne.
Le contrôleur orchestre : il reçoit l'opération système et décide de l'ordre des choses. Il ne calcule pas, ne compare pas, ne totalise pas. Le module Le diagramme de classes & l'architecture logique avait déjà repéré son cousin — le contrôleur dont la liste d'opérations ressemble au sommaire du système. C'est la même maladie vue d'un autre diagramme, et Les principes GRASP lui donne son nom officiel.
Ce que le diagramme renvoie au diagramme de classes
On arrive à la raison d'être de tout ce qui précède. Le module Le diagramme de classes & l'architecture logique laissait une question ouverte : d'où viennent les opérations des classes ? Il donnait une réponse partielle — celles qui découlent des attributs, celles qui viennent d'une règle du métier — et renvoyait le reste à cette semaine. Voici la règle qui manquait, et elle est purement mécanique :
Tout message reçu par un objet est une opération de sa classe. Pas le message qu'il envoie : celui qu'il reçoit. La flèche pointe vers celui qui devra savoir faire.
Le geste, une fois le diagramme dessiné, se fait donc au crayon et sans réfléchir : on relève chaque flèche, on regarde où sa pointe arrive, et on inscrit une ligne dans la boîte correspondante.
| Le message | Reçu par | La ligne à écrire dans la boîte |
|---|---|---|
confirmerEmprunt() |
ControleurEmprunt |
+ confirmerEmprunt(lecteur : Lecteur, livre : Livre) : Date |
estDisponible() |
Livre |
+ estDisponible() : boolean |
emprunter() |
Livre |
+ emprunter() : void |
«create» |
Emprunt |
le constructeur — on ne l'écrit pas comme une opération ordinaire |
La visibilité se déduit du même dessin. Une opération que quelqu'un
d'autre appelle est publique : +. Une opération qui n'apparaît que dans
un auto-appel n'est utile qu'à sa propre classe, et elle peut donc être
privée : -. C'est un des rares endroits du cours où une décision de
conception se lit directement sur un dessin, sans avoir à en discuter.
Et les liens remontent aussi. Pour envoyer un message à un objet, il faut
pouvoir l'atteindre : chaque lien de ton diagramme de communication annonce donc une
association navigable ou une dépendance dans le diagramme de classes. Si
:ControleurEmprunt parle à :Livre, une flèche devra exister
entre les deux boîtes — et c'est aussi comme ça qu'on s'aperçoit qu'un diagramme
d'interaction trop bavard produira un diagramme de classes trop couplé.
D'après ces trois messages, quelle ligne faut-il ajouter dans la boîte
Lecteur, et quelle ligne ne faut-il surtout pas y mettre ?
1 :ControleurEmprunt ──▶ :Lecteur
estEnRegle()
2 :Lecteur - -▶ :ControleurEmprunt
vrai
3 :Lecteur ──▶ :Lecteur
totalAmendes()
Deux lignes vont dans Lecteur, et leur visibilité
diffère : + estEnRegle() : boolean, publique parce qu'une autre
classe l'appelle ; et - totalAmendes() : double, privée parce
qu'elle n'apparaît que dans un auto-appel — personne d'autre que le lecteur ne s'en
sert.
Ce qu'il ne faut surtout pas y mettre : le retour de la ligne 2.
vrai n'est pas un message, c'est une valeur qui remonte ; elle est
déjà décrite par le : boolean de la première ligne. L'autre faute du
même genre serait d'écrire estEnRegle() dans la boîte
ControleurEmprunt sous prétexte que c'est lui qui l'envoie. La règle ne
regarde jamais l'émetteur : elle regarde où la pointe arrive.
Mets bout à bout ce que les quatre modules précédents ont produit. Le modèle du domaine a donné les concepts. Le cas d'utilisation & DSS a donné les opérations système. Le diagramme de classes a posé les boîtes, en laissant leur troisième compartiment à moitié vide. Les diagrammes d'interaction le remplissent, et la boucle se ferme : c'est le même document qui repart à chaque tour, un peu plus complet.
L'idée déborde largement d'UML. Découvrir ce qu'un composant doit savoir faire en regardant qui lui parle, c'est la manière normale de concevoir une interface — l'API d'un service web se dessine ainsi, en listant les appels que ses clients feront avant d'écrire une seule ligne du serveur. Tu ne demandes jamais à un objet ce qu'il veut offrir : tu regardes ce qu'on lui demande.
Le dessiner sur une copie, sans notes
Un mot sur la forme de ce module avant de finir, parce qu'elle n'est pas celle de l'examen. Ici, les messages s'écrivent en pile — une ligne chacun, de haut en bas —, parce qu'un diagramme à quatre colonnes ne tient pas sur un écran de téléphone. Sur ta copie, tu dessineras des colonnes. La correspondance est directe et il suffit de l'avoir vue une fois : chaque nom d'objet qui apparaît dans la pile devient une boîte en haut de la feuille, et chaque ligne de la pile devient une flèche horizontale entre deux colonnes, tracée d'autant plus bas qu'elle vient plus tard.
L'ordre des gestes compte, parce qu'il évite de tout gommer :
- Les lignes de vie d'abord, de gauche à droite : l'acteur, puis le contrôleur, puis les entités dans l'ordre où elles entreront en jeu. Cette disposition n'est pas obligatoire, mais elle rend les flèches presque toutes orientées vers la droite — donc lisibles.
- Les messages ensuite, du haut vers le bas, un par un, sans sauter de ligne. Nomme-les complètement dès le premier passage : c'est ce qu'on oublie quand le temps manque.
- Les barres d'activation en avant-dernier. Elles se déduisent des messages — chacune commence à une réception et finit à une réponse —, donc les tracer avant oblige à les corriger.
- Les cadres, les gardes et les retours pour finir. Un cadre se dessine plus facilement quand on sait déjà quels messages il doit entourer.
Puis la relecture, qui prend une minute et rapporte plus que n'importe quel raffinement. Six questions, dans cet ordre :
- Le premier message est-il l'opération système ? Et arrive-t-il bien sur le contrôleur ?
- Chaque objet porte-t-il le deux-points ?
Livresans deux-points n'est pas un objet, c'est la classe. - Chaque flèche part-elle de celui qui demande ? Une flèche à l'envers change complètement qui devra savoir faire.
- Chaque condition est-elle dans une garde ? Et chaque garde dans un cadre ?
- Les barres s'emboîtent-elles ? Celle de l'appelant doit couvrir celle de l'appelé, du début à la fin.
- L'acteur reçoit-il quelque chose à la fin ? Le DSS lui avait promis une réponse ; ton diagramme doit la produire.
Faire travailler l'acteur. On lui envoie
afficherConfirmation(), on lui fait vérifier quelque chose, on lui donne
une barre d'activation qui dure. C'est tentant parce qu'il est sur le dessin comme les
autres.
Mais l'acteur est dehors : il déclenche et il reçoit, un point
c'est tout. Un message qui part vers lui ne peut être qu'un retour, et ce
retour porte une valeur — dateRetour —, jamais une consigne d'affichage.
C'est la même règle qu'au DSS, où un message sortant décrivait ce que le système
rend et non comment il l'affiche : elle ne change pas quand la boîte
noire s'ouvre.
Cette matière est celle de la semaine 5, é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. C'est aussi le cœur du Livrable 2 — Modèle de conception et Architecture logique, à remettre le 13 octobre (10 %) : le « modèle de conception » de son titre, ce sont ces diagrammes-ci, et le diagramme de classes qu'ils remplissent. Chez Larman, ce sont les chapitres 14 et 15.
IFT-2007 n'autorise aucun matériel. Quatre choses doivent donc être en
tête, et ce sont exactement celles qu'on peut rater en ayant compris la
leçon : à quoi ressemblent les trois flèches, ce que la barre d'activation dit de
plus que la ligne de vie, la différence entre alt et opt, et
la façon dont se numérote un diagramme de communication.
« Réalisez l'opération système enregistrerRetour(isbn) par un diagramme de séquence. »
L'énoncé le plus long du chapitre, et il se traite dans l'ordre qui rapporte des points même quand le résultat diffère du corrigé. Le message initial part de l'acteur vers le contrôleur ; écris ensuite en marge, en français, la liste de ce qu'il faut faire ; pour chaque item, demande-toi qui possède l'information et trace la délégation ; termine par le retour vers l'acteur.
Ce qui distingue une bonne copie : les entités y font quelque chose. Un diagramme où toutes les flèches partent du contrôleur montre qu'on a su dessiner mais pas concevoir — et cette faute-là se voit de loin, sans même lire les noms.
« Convertissez ce diagramme de séquence en diagramme de communication. »
Une conversion, pas une reconstruction : les objets et les messages sont les mêmes, on change seulement ce qui porte l'ordre. Pose les objets, relie ceux qui se parlent par un trait, puis recopie chaque message sur son lien avec son numéro et une flèche de sens.
Les deux points où l'on perd des points : oublier la numérotation
imbriquée — 1.1, 1.2 pour ce qui part pendant le
traitement de 1, et non 2, 3 à plat —, et
laisser tomber les fragments au passage. Un loop devient une astérisque
sur le numéro, une garde revient entre crochets ; rien ne doit disparaître, sinon
la conversion a perdu de l'information.
« Quelle est la différence entre un message synchrone et un message asynchrone ? »
La réponse en une phrase : l'émetteur d'un message synchrone attend la réponse avant de continuer ; celui d'un message asynchrone continue immédiatement. Puis la notation : pointe pleine contre pointe fine, sur un trait plein dans les deux cas.
Ajoute la conséquence, qui montre qu'on a compris plutôt que récité : un appel de méthode ordinaire en Java est synchrone, donc c'est la pointe pleine qu'on trace par défaut — et un message asynchrone ne peut pas porter de retour, puisqu'il n'y a plus personne pour l'attendre.
Un diagramme d'interaction ouvre la boîte noire du DSS pour une opération système, et montre les objets qui se la partagent. Il en existe deux formes qui disent la même chose — la séquence, où l'ordre se lit de haut en bas, et la communication, où il tient dans des numéros —, et savoir passer de l'une à l'autre est une compétence d'examen à part entière. Toute la notation tient dans quatre pièces : la ligne de vie, le message, le retour, la barre d'activation ; les fragments ajoutent le si et le tant que en entourant des messages qui, eux, ne changent pas.
Mais l'essentiel n'est pas le dessin. Dessiner l'interaction, c'est décider qui fait quoi, et cette décision se prend avec une seule question posée à chaque étape : qui possède déjà l'information nécessaire ? Un contrôleur d'où partent toutes les flèches est la trace visible qu'on ne se l'est pas posée. Ce que le diagramme laisse derrière lui, c'est la liste des messages reçus — et chacun devient une opération dans une boîte du diagramme de classes, qui n'est complet qu'à ce moment-là.
Et ailleurs : la question « qui envoie quoi à qui, et dans quel ordre ? » est celle qu'on se pose devant n'importe quel système à plusieurs pièces, longtemps après avoir oublié UML. C'est ce que montre une trace d'exécution, un journal de requêtes HTTP, un onglet réseau de navigateur : des messages, des attentes, des réponses. Savoir lire une chronologie d'échanges, et repérer celui qui fait le travail des autres, sert à concevoir comme à déboguer.
- Ce que dit la barre posée sur une ligne de vie
- que l'objet exécute. Le trait pointillé, lui, dit seulement qu'il existe. Tant qu'un objet attend une réponse, sa barre continue : elles s'emboîtent
- Ce qui différencie un message synchrone, un asynchrone et un retour
- trait plein + pointe pleine = synchrone, l'émetteur attend · trait plein + pointe fine = asynchrone, il continue · trait pointillé = retour, et il porte une valeur
- La forme complète d'un message
[garde] valeur := nom(arg : Type)— seul le nom est obligatoire- Ce que veut dire le deux-points de
:Livre - « instance de ». Sans lui,
Livredésigne la classe, pas un objet ; avec un nom devant,emprunte:Livre, on distingue deux objets de la même classe altcontreoptalt: plusieurs branches séparées par un trait pointillé, une seule s'exécute ·opt: une branche unique, qui a lieu ou n'a pas lieu. Il n'y a pas de « sinon » dans unopt- Comment se numérote un diagramme de communication
1, puis1.1et1.2pour ce qui part pendant le traitement de1. L'astérisque marque la répétition ; la garde s'écrit avant les deux-points- Ce qui porte l'ordre dans chacune des deux notations
- en séquence, la position verticale — et rien d'autre n'est à l'échelle · en communication, le numéro, la position sur la page ne voulant rien dire
- Ce que devient un message reçu, dans le diagramme de classes
- une opération de la classe qui le reçoit — jamais de celle qui l'envoie. Publique si elle vient d'ailleurs, privée si elle ne vient que d'un auto-appel
- Qui reçoit le premier message, et combien de diagrammes par cas d'utilisation
- le contrôleur, et ce premier message est une opération système : il y a donc un diagramme par flèche entrante du DSS
- Comment se notent la naissance et la mort d'un objet
«create»vers sa boîte, qui se dessine plus bas que les autres ; un×au bout de la ligne de vie pour la destruction