Skip to content

Commit 27a82df

Browse files
committed
feat: Le design pattern Command and command handler
1 parent d68274f commit 27a82df

File tree

6 files changed

+188
-12
lines changed

6 files changed

+188
-12
lines changed
Loading

content/blog/command-handler-patterns/explain-command-handler.svg

Lines changed: 0 additions & 1 deletion
This file was deleted.

content/blog/command-handler-patterns/index.en.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
author: "Arnaud Langlade"
33
title: "Command and command handler design pattern"
4-
url: "command-handler-patterns.html"
5-
aliases: ["command-handler-patterns"]
4+
url: "command-handler-patterns"
5+
aliases: ["command-handler-patterns.html"]
66
date: "2021-02-25"
77
image_credit: redaquamedia
8-
description: "Discover the command design pattern in software development. Learn how commands represent user intents, handled by command handlers. Learn practical tips, examples, and insights for efficient command validation."
8+
description: "Discover the command and command handler design pattern in software development. Commands represent user intentions, which are handled by the command handler. Learn about these patterns through concrete examples and more, such as command validation or the use of a command bus"
99
keywords: "design patterns,software,software architecture,command,command handler,command bus"
1010
tags: [
1111
"command-bus",
@@ -27,7 +27,7 @@ A command handler is just a callable that executes all the actions needed to ful
2727

2828
## How does it work?
2929

30-
{{< image src="explain-command-handler.svg" alt="Command handler design pattern" >}}
30+
{{< image src="explain-command-handler.png" alt="Command handler design pattern" >}}
3131

3232
This pattern has some rules. The first one is that a command can be handled by a single command handler because there is only a single way to handle a use case. The second rule is that a command handler should receive a valid command. Validating the command ensures that the user provides the correct data to prevent the handling from failing. It also helps to provide early feedback to the user about the data they provided.
3333

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
---
2+
author: "Arnaud Langlade"
3+
title: "Le design pattern Command and command handler"
4+
url: "command-handler-patterns"
5+
date: "2024-03-26"
6+
image_credit: redaquamedia
7+
description: "Découvrez le design pattern command et command handler dans le développement logiciel. Les commandes représentent les intentions des utilisateurs, prises en charge par le command handler. Apprenez ces patterns à travers des exemples concrets et bien plus, comme par exemple la validation de commande ou l’utilisation d’un command bus"
8+
keywords: "design patterns,software,software architecture,command,command handler,command bus"
9+
tags: [
10+
"command-bus",
11+
"design-patterns",
12+
]
13+
---
14+
15+
Ce modèle est vraiment intéressant et peut vous aider à gérer vos cas d'utilisation (use cases). Une `command` représente l'intention de l'utilisateur, tandis que le `command handler` réalise les actions nécessaires pour accomplir le cas d'utilisation. Approfondissons un peu ces deux concepts.
16+
17+
## Qu'est-ce qu'une commande ?
18+
19+
Une commande est un objet qui encapsule toutes les informations nécessaires pour exécuter une action. Ce design pattern sert à représenter les intentions des utilisateurs, et la commande est ensuite donnée à un command handler.
20+
21+
Généralement, une commande est conçue comme un Data Transfer Object (DTO), c'est-à-dire un objet sans comportement propre (une structure de données). La règle de conception la plus importante est que la commande doit être facilement sérialisable, ce qui permettra de l’envoyer dans une file d'attente orchestrée par des outils comme RabbitMQ ou pub-sub pour un traitement asynchrone.
22+
23+
## Qu'est-ce qu'un command handler
24+
25+
Un command handler est simplement un callable qui réalise toutes les actions nécessaires pour satisfaire l'intention d'un utilisateur. Comme vous pouvez le comprendre, ce design pattern est idéal pour gérer vos cas d'utilisation métier (business use cases).
26+
27+
## Comment cela fonctionne-t-il ?
28+
29+
{{< image src="explain-command-handler.png" alt="Le design pattern Command and command handler" >}}
30+
31+
Ce pattern suit quelques règles essentielles. La première stipule qu'une commande doit être traitée par un unique command handler car il existe une seule façon pour traiter un cas d'utilisation spécifique. La seconde règle précise que le command handler doit recevoir une commande valide. La validation de la commande garantit que les données fournies par l'utilisateur sont correctes, évitant ainsi l'échec de l’exécution de la commande. Cela permet également de fournir rapidement un retour à l'utilisateur concernant les données qu'il a soumises.
32+
33+
La commande est simplement un DTO qui transporte des données, tandis que le command handler est chargé d'exécuter toutes les actions nécessaires pour réaliser les cas d'utilisation.
34+
35+
## Comment l'utiliser ?
36+
37+
Considérons un exemple simple : la création d’un compte. Notre expert métier s'attend à ce que les utilisateurs fournissent un email et un mot de passe pour créer un compte pour se connecter sur l’application. Nous allons concevoir une commande nommée `CreateAnAccount` et son handler, `CreateAnAccountHandler`.
38+
39+
Tout d'abord, nous devons créer une commande nommée `CreateAnAccount` pour représenter l'intention de l'utilisateur.
40+
41+
```php
42+
final class CreateAnAccount
43+
{
44+
public readonly string $username;
45+
public readonly string $password;
46+
47+
public function __construct(string $username, string $password)
48+
{
49+
$this->username = $username;
50+
$this->password = $password;
51+
}
52+
}
53+
```
54+
55+
Ensuite, il est nécessaire de créer un command handler pour prendre en charge ce cas d'utilisation. Le command handler peut être une fonction ou un objet invocable. Il ne doit retourner aucun résultat (void) afin d'être traité de manière asynchrone, puisque nous ne connaissons pas le moment où son traitement sera fini et qu'un résultat instantané ne peut être garanti. En utilisant les données fournies par la commande, nous effectuons toutes les actions requises pour adresser le cas d'utilisation. Dans notre exemple, nous constituons un agrégat `Account` que nous passerons ensuite au repository `Account`.
56+
57+
```php
58+
final class CreateAnAccountHandler
59+
{
60+
private Accounts $accounts;
61+
62+
public function __construct(Accounts $accounts)
63+
{
64+
$this->accounts = $accounts;
65+
}
66+
67+
public function __invoke(CreateAnAccount $createAnAccount): void
68+
{
69+
$account = Account::create(
70+
$createAnAccount->username(),
71+
$createAnAccount->password()
72+
);
73+
74+
$this->accounts->add($account);
75+
}
76+
}
77+
```
78+
79+
Pour finir, assemblons ces morceaux de code dans un contrôleur (cet exemple est fait avec le Framework Symfony). Ce contrôleur reçoit des données encodées en JSON pour créer une commande, qui est ensuite validée et passée au handler.
80+
81+
```php
82+
final class CreateAnAccount
83+
{
84+
// ...
85+
86+
public function __invoke(Request $request): Response
87+
{
88+
$command = $this->serializer->deserialize(
89+
$request->getContent(),
90+
CreateAnAccount::class,
91+
'json'
92+
);
93+
94+
$violations = $this->validator->validate($command);
95+
96+
if (0 < $violations->count()) {
97+
throw new BadRequestHttpException(/*json encoded violation*/);
98+
}
99+
100+
($this->createAnAccountHandler)($command);
101+
102+
return new JsonResponse(null, Response::HTTP_CREATED);
103+
}
104+
}
105+
```
106+
107+
**Astuce :** Pour faciliter la création de commandes, vous pouvez utiliser des bibliothèques comme le composant Serializer de Symfony. Cela simplifie la création d'objets à partir d'un set de données (tel que JSON), la rendant ainsi plus aisé et rapide
108+
109+
```php
110+
$createAccount = $serializer->deserialize(
111+
'{“username”:”arnaud”, “password”:“password”}',
112+
CreateAnAccount::class,
113+
'json'
114+
);
115+
```
116+
117+
**Astuce :** Afin d'éviter de réinventer la roue, vous pouvez vous appuyer sur des bibliothèques telles que le composant `Validator` de Symfony pour effectuer la validation de la commande.
118+
119+
```php
120+
$violation = $validator->validate($createAccount);
121+
```
122+
123+
J'ai écrit un article de blog expliquant comment valider une commande :
124+
125+
{{< page-link page="how-to-validate-a-command" >}}
126+
127+
## Comment simplifier cela
128+
129+
Pour simplifier ce contrôleur, vous pouvez utiliser un command bus qui sera responsable de déterminer le bon command handler pour une commande donnée. Pour plus d'informations sur ce design pattern, j'ai écrit un article de blog expliquant comment cela fonctionne :
130+
131+
{{< page-link page="command-bus-design-pattern" >}}
132+
133+
L'exemple suivant est construit avec [Symfony Messenger](https://symfony.com/doc/current/components/messenger.html).
134+
135+
```php
136+
public function __invoke(Request $request): Response
137+
{
138+
$command = $this->serializer->deserialize(
139+
$request->getContent(),
140+
CreateAnAccount::class,
141+
'json'
142+
);
143+
144+
$this->commandBus->handle($command);
145+
146+
return new JsonResponse(null, Response::HTTP_CREATED);
147+
}
148+
```
149+
150+
Où se situe la validation de la commande dans cet exemple ? Les command buses sont souvent construits de middlewares, ce qui les rend facilement configurables. Afin de s'assurer que toutes les commandes sont valides avant de les donner à un command handler, il est nécessaire d'ajouter un middleware au command bus dédié à la validation des commandes.
151+
152+
```php
153+
class ValidationMiddleware implements MiddlewareInterface
154+
{
155+
// …
156+
157+
public function handle(Envelope $envelope, StackInterface $stack): Envelope
158+
{
159+
$message = $envelope->getMessage();
160+
$violations = $this->validator->validate($message, null, $groups);
161+
if (\count($violations)) {
162+
throw new ValidationFailedException($message, $violations);
163+
}
164+
165+
return $stack->next()->handle($envelope, $stack);
166+
}
167+
}
168+
```
169+
170+
**Astuce :** Jetez un œil à ce blog post si vous devez gérer les permissions des utilisateurs. Ajouter un middleware au command bus peut renforcer la sécurité de votre application.
171+
172+
{{< page-link page="how-to-handle-user-permissions-through-command-bus-middleware/" >}}
173+
174+
## Pour finir
175+
176+
Dans de nombreuses applications, j'ai vu beaucoup de classes nommées managers ou services (par exemple, `AccountService`, `AccountManager`) qui regroupent toute la gestion des cas d'utilisation dans une seule classe. Bien que cette approche puisse être efficace au départ, au fur et à mesure que le développement progresse, ces classes ont tendance à devenir de plus en plus grandes et à devenir un "god object". Cela rend la maintenance difficile, réduit la lisibilité et peut rapidement se transformer en objet fourre-tout. Je pense que l'adoption de es pattern peut résoudre ces problèmes

layouts/partials/content-image.html

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@
1111
{{ $format = (printf "%s%s%s" "resize " . " webp" ) }}
1212
{{ end }}
1313

14-
{{ with .context.Resources.GetMatch $src }}
15-
{{ with .Process $format }}
16-
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="{{ $alt }}" class="{{ $class }} img-fluid">
17-
{{ end }}
18-
{{ else }}
14+
{{ if (strings.Contains .src ".svg") }}
1915
<img class="{{ $class }} img-fluid" src="{{ .src }}" alt="{{ .alt }}" />
16+
{{ else }}
17+
{{ with .context.Resources.GetMatch $src }}
18+
{{ with .Process $format }}
19+
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="{{ $alt }}" class="{{ $class }} img-fluid">
20+
{{ end }}
21+
{{ end }}
2022
{{ end }}
21-

layouts/shortcodes/image.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<p class="text-center">
2-
{{ partial "content-image" (dict "src" (.Get "src") "alt" (.Get "alt") "class" (.Get "class")) }}
2+
{{ partial "content-image" (dict "context" $.Page "src" (.Get "src") "alt" (.Get "alt") "class" (.Get "class")) }}
33
</p>

0 commit comments

Comments
 (0)