Les trois familles de messages
Le chapitre précédent opposait la commande et l'événement. Mais demander une information n'est ni l'un ni l'autre. Voici la troisième famille, et la ligne nette qui les sépare toutes les trois.
Au chapitre précédent, on a séparé deux familles de messages : la commande, qui ordonne (« Prélève le paiement »), et l’événement, qui constate (« Commande payée »). Mais reprends le scénario juste avant le paiement : le service de commande doit d’abord vérifier que la carte du client n’est pas expirée. « La carte est-elle valide ? » n’ordonne rien et ne constate aucun fait passé. Comment nommer ce geste, et où le ranger ?
La question manquante
Quand ton service de commande demande « La carte du client est-elle valide ? », il ne cherche pas à changer le monde : il cherche à le connaître. Il attend une réponse, ici un simple oui ou non, et il en a besoin tout de suite pour décider de la suite. Ce n’est pas un ordre, car rien n’est modifié. Ce n’est pas un fait passé que l’on diffuse, car personne d’autre n’est concerné et l’émetteur attend un retour précis.
C’est une troisième intention, aussi courante que les deux autres : demander. Un message de requête Message de requête Message qui demande une information à un destinataire précis sans rien changer à l'état du système (par exemple « Quel est le solde de fidélité de ce client ? »). Il se formule comme une question, vise un seul gestionnaire, et l'émetteur attend toujours une donnée en retour. C'est la troisième famille de messages, à côté de la commande qui ordonne et de l'événement qui constate. Source : Hohpe & Woolf, 2003 pose une question à un destinataire précis et attend toujours une donnée en retour, sans rien changer à l’état du système. « Quel est le solde de fidélité de ce client ? », « Cette adresse de livraison existe-t-elle ? », « Combien reste-t-il en stock ? » : autant de requêtes.
L’analogie du chapitre zéro tient toujours, mais il faut l’affiner. La commande, c’est déposer un mot dans une boîte aux lettres. La requête, c’est se présenter à un guichet : on pose sa question, et on reste là, le temps que l’employé consulte son registre et réponde. On attend la réponse, mais on n’a rien demandé de changer.
Lire ou écrire : la ligne de partage
Pourquoi séparer la requête de la commande, alors que les deux s’adressent à un seul destinataire ? Parce qu’elles ne font pas la même chose au système. La commande écrit : elle change un état (un paiement prélevé, un stock diminué). La requête lit : elle renvoie une information sans rien toucher.
Cette distinction porte un nom et un principe. La séparation commande-requête Séparation commande-requête Principe de conception selon lequel une opération doit soit changer l'état du système sans rien renvoyer d'utile (une commande), soit renvoyer une donnée sans rien changer (une requête), jamais les deux à la fois. Énoncé par Bertrand Meyer (Command-Query Separation, CQS), il rend chaque message lisible : on sait au premier coup d'oeil s'il écrit ou s'il lit. C'est la ligne qui sépare proprement la commande de la requête. Source : Meyer, 1988 , énoncée par Bertrand Meyer en 1988, dit qu’une opération doit faire l’un ou l’autre, jamais les deux : soit elle change l’état et ne renvoie rien d’utile (commande), soit elle renvoie une donnée et ne change rien (requête). Une méthode qui réserve le stock et renvoie le stock restant mélange les deux rôles, et c’est exactement ce qu’on cherche à éviter.
Le gain est immense pour raisonner : une requête est rejouable sans risque (la relancer dix fois ne change rien), une commande ne l’est pas (la rejouer prélève dix fois). Savoir, au premier coup d’oeil, si un message lit ou écrit, c’est savoir s’il est dangereux de le répéter. On y reviendra longuement avec les sémantiques de livraison.
Trois familles, deux questions
On a maintenant tout pour ranger n’importe quel message. Deux questions suffisent, posées dans l’ordre.
La première question porte sur le destinataire : le message est-il adressé à un seul destinataire connu, ou diffusé à des abonnés inconnus ? Si l’émetteur ne sait pas qui écoute et n’attend aucun retour, c’est un événement, et la seconde question ne se pose même pas. La diffusion est le trait qui isole l’événement.
La seconde question ne sert qu’aux messages adressés : ce message écrit-il l’état, ou le lit-il seulement ? S’il écrit, c’est une commande. S’il lit, c’est une requête.
Voici les trois familles côte à côte, sur les critères qui comptent.
| Critère | Commande | Requête | Événement |
|---|---|---|---|
| Intention | Ordonner | Demander | Constater |
| Temps du verbe | Impératif | Interrogatif | Passé |
| Exemple | « Prélève le paiement » | « La carte est-elle valide ? » | « Commande payée » |
| Destinataire | Un seul, connu | Un seul, connu | Inconnus, diffusé |
| Effet sur l’état | Écrit | Lit | Constate un fait déjà écrit |
| Réponse attendue | Un effet, parfois un résultat | Toujours une donnée | Aucune |
| Nombre de gestionnaires | Un | Un | Zéro à plusieurs |
Une remarque utile pour ne pas se tromper : commande et requête se ressemblent par la forme (un destinataire unique, un échange précis), mais s’opposent par l’effet (écrire contre lire). Requête et événement se ressemblent par l’absence d’ordre, mais s’opposent par la portée (un destinataire contre une diffusion). Chaque paire partage un trait et en oppose un autre : c’est pourquoi il faut les deux questions, pas une seule.
Range les messages toi-même
Le composant ci-dessous te donne six messages tirés de notre commande e-commerce. Fais glisser chacun dans l’un des trois bacs, ou sers-toi des boutons. Quand tu as fini, vérifie : chaque carte mal classée révèle sa vraie famille et la raison, sur les deux axes que l’on vient de poser.
Fais glisser chaque message dans la bonne famille, ou utilise les boutons. Vérifie ensuite : chaque carte mal rangée révèle sa vraie famille et pourquoi.
Messages à classer
- Prélève le paiement
- La carte du client est-elle valide ?
- Commande payée
- Réserve le stock
- Quel est le solde de fidélité du client ?
- Stock réservé
Commande
Requête
Événement
Trois questions à te poser en classant :
- Les deux requêtes (la validité de la carte, le solde de fidélité) ne changent rien. Qu’est-ce qui te permet de les distinguer des commandes, qui leur ressemblent par la forme ?
- « Commande payée » et « Stock réservé » sont au passé. Pourquoi sont-ils des événements et non des commandes, alors qu’ils parlent bien d’une écriture ?
- Si tu hésites sur une carte, quelle question poses-tu en premier : « écrit ou lit ? » ou « adressé ou diffusé ? » Pourquoi l’ordre compte-t-il ?
Côté code : trois familles, trois acheminements
Les frameworks de messaging ne se contentent pas de distinguer les familles sur le papier : ils les acheminent différemment. Et c’est ici qu’apparaît une pièce dont on s’est servi au chapitre zéro sans la nommer. Le médiateur Médiateur Objet qui centralise l'acheminement des messages à l'intérieur d'un même processus, en mémoire. Au lieu que l'émetteur référence directement le bon gestionnaire, il remet le message au médiateur, qui sait à quel handler le router : un seul pour une commande ou une requête, zéro à plusieurs abonnés pour un événement. Il se distingue du bus ou broker, qui rend le même service mais à travers le réseau, entre processus. Source : Gamma et al., 1994 est l’objet qui, en mémoire et dans un seul processus, reçoit un message et le route vers le bon gestionnaire : un seul pour une commande ou une requête, zéro à plusieurs pour un événement.
Avec Hexeract, un framework de messaging en Rust, chaque famille a son verbe sur le médiateur. La requête, nouvelle ici, se déclare avec le trait Query et un type de retour, puis s’envoie avec query :
// Requête : on lit, on attend une donnée, rien n'est modifié.
impl Query for GetLoyaltyBalance {
type Output = LoyaltyPoints;
}
let balance = mediator.query(GetLoyaltyBalance { customer_id }).await?;
// Pour mémoire, les deux familles du chapitre zéro :
let payment_id = mediator.send(ProcessPayment { order_id, amount_cents }).await?; // commande
mediator.publish(OrderPaid { order_id }).await?; // événement
Le trio est limpide : send pour ordonner, query pour demander, publish pour diffuser. La séparation commande-requête est inscrite dans le type : Command et Query sont deux traits distincts, et le compilateur refuse de confondre l’un avec l’autre.
Avec Wolverine, son équivalent en .NET, la requête n’a pas de verbe dédié : elle emprunte le canal de la demande-réponse. On envoie un message et on attend sa réponse typée avec la même méthode que la commande qui renvoie un résultat :
// Requête : une demande qui renvoie une réponse typée (request-reply).
var balance = await bus.InvokeAsync<LoyaltyBalance>(new GetLoyaltyBalance(customerId));
// La commande qui renvoie un résultat utilise le même verbe.
var status = await bus.InvokeAsync<PaymentStatus>(new ProcessPayment(orderId, amountCents));
// L'événement, lui, ne renvoie rien.
await bus.PublishAsync(new OrderPaid(orderId));
Deux philosophies, donc. Hexeract fait de la requête un type de premier rang, distinct de la commande. Wolverine la traite comme un cas de la requête-réponse Requête-réponse Schéma d'échange où l'émetteur envoie une requête puis attend, sur un canal de retour, la réponse correspondante. C'est le motif naturel de la requête : « Quel est le solde ? » appelle « 240 points ». Pour relier chaque réponse à sa requête quand plusieurs circulent, on attache souvent un identifiant de corrélation. Décrit par Hohpe & Woolf sous le nom Request-Reply. Source : Hohpe & Woolf, 2003 : envoyer quelque chose et attendre une réponse corrélée. Les deux sont légitimes, et retenir cette différence t’évitera bien des débats stériles : ce qui compte n’est pas le verbe du framework, mais l’intention que tu as identifiée avant de l’écrire.
Exercices
Prends une feuille et un crayon. Les corrigés sont juste en dessous, à ne regarder qu’après avoir essayé.
Exercice 1 : classer six messages
Classe chacun de ces six messages en commande, requête ou événement, en posant les deux questions dans l’ordre (adressé ou diffusé, puis écrit ou lit) : (a) « Annule la commande », (b) « Le client est-il majeur ? », (c) « Paiement refusé », (d) « Calcule les frais de port », (e) « Quel est le statut de la livraison ? », (f) « Email envoyé ».
Exercice 2 : réparer une violation de la séparation commande-requête
Un développeur a écrit une seule opération ReserveStockAndGetRemaining qui, en un appel, réserve trois unités d’un produit et renvoie le nombre d’unités restantes. Explique en quoi elle viole la séparation commande-requête, puis propose le découpage en deux messages, en nommant chacun et en indiquant sa famille.
En une phrase
Tout message appartient à l’une de trois familles, que l’on distingue par deux questions : diffusé à des inconnus, c’est un événement ; adressé à un seul destinataire, c’est une commande s’il écrit l’état, une requête s’il le lit seulement.
1. Quelle question distingue une commande d'une requête, une fois qu'on sait que les deux sont adressées à un destinataire unique ?
2. Pourquoi une requête est-elle sans danger à rejouer, contrairement à une commande ?
3. Comment Hexeract et Wolverine expriment-ils la requête, et que faut-il en retenir ?
Vers le chapitre suivant
On sait maintenant quoi on s’envoie : des commandes, des requêtes, des événements. Mais depuis le début, on traite « la boîte aux lettres » comme une boîte noire qui accepte les dépôts et les distribue. Or il existe deux grandes façons de construire cette boîte, deux philosophies qui ne tiennent pas les mêmes promesses. La première, le courtier, distribue chaque message à un consommateur puis l’oublie, comme un facteur. La seconde, le journal, conserve tous les messages dans l’ordre, comme un registre que chacun relit à son rythme. Le chapitre deux ouvre la boîte et confronte ces deux mondes, RabbitMQ contre Kafka.
Sources
- Meyer, B. (1988). Object-Oriented Software Construction. Prentice Hall. Principe de séparation commande-requête (Command-Query Separation). Référence de l’éditeur
- Hohpe, G. & Woolf, B. (2003). Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions. Addison-Wesley. Référence de l’éditeur
- Hohpe, G. & Woolf, B. « Command Message » et « Request-Reply ». Enterprise Integration Patterns. Command Message, Request-Reply
- Wolverine. Message Bus Basics: InvokeAsync, SendAsync, PublishAsync. Documentation officielle. wolverinefx.net