Modèle de conception Commande en langage Groovy
Le modèle de conception Commande fait partie des modèles de conception de type comportemental, présenté notamment dans le célèbre livre Design Patterns: Elements of Reusable Object-Oriented Software de Erich Gamma, Richard Helm, Ralph Johnson, et John Vlissides.
Dans cet article, je donnerai un exemple d'implémentation de ce modèle de conception en langage Groovy, dans le style de programmation orientée objet, puis ensuite dans le style de la programmation fonctionnelle, avec beaucoup moins de "cérémonie" (moins de déclarations de type).
Approche classique dans le style de la programmation orientée objet
Un objet de type Commande permet de représenter une action à effectuer, avec tous les paramètres nécessaires à son exécution ; comme représenté dans le diagramme UML ci-dessous (provenant de la page Command pattern sur Wikipedia), de tels objets sont des instances de classes implémentant une interface commune (ici Command
) définissant une méthode représentant l'exécution de l'action.
On peut ainsi créer de tels objets représentant des actions qui seront à n'importe quel moment exécutées via la méthode execute
de l'interface Command
, par un client qui a le type Invoker
dans le diagramme. Il n'y a ainsi pas de couplage entre le client et une action implémentée, car l'interaction s'effectue par l'intermédiaire d'une interface.
Le diagramme porte aussi une notion de receveur, appelé également cible, sous la forme d'une classe : celle-ci représente la logique métier sur laquelle l'action s'appuie pour effectuer son traitement.
Gestion de panier d'achats
Imaginez que, dans une application de commerce en ligne, nous voulions garder un historique de toutes les actions d'un utilisateur sur son panier d'articles, en ne considérant -par simplification- que l'ajout et le retrait d'un article dans le panier.
Dans le code ci-dessous, que vous pourrez toujours exécuter en tant que script Groovy, nous avons les types suivants :
- La classe
Item
représentant un article à ajouter au panier ou à l'en retirer. - La classe
Cart
permettant la gestion du panier, et codée de manière simpliste ; cette classe a le rôle de receveur. - Le trait
CartCommand
(qui nous permet de partager la propriétécart
, tout comme nous aurions pu également partager une propriétéitem
), et les classes actionAddItemCommand
etRemoveItemCommand
pour respectivement, représenter des actions d'ajout et de retrait d'article.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
|
Dans le script ci-dessus, nous créons trois commandes représentant des actions utilisateur au cours de sa session d'achat puis, de manière différée, nous exécutons chacune d'elles (grâce à l'opérateur spread), de sorte qu'au final, nous avons dans l'objet Cart
l'état final du panier.
En l'occurrence, nous ajoutons l'article "Item1", puis l'article "Item2", et enfin nous retirons le premier article, ce qui fait que le panier ne contient plus que l'article "Item2", ce qui est vérifié par l'assertion.
Macro commande
Pour faciliter l'exécution d'un ensemble de commandes, mais aussi afin de pouvoir définir un autre receveur sur chacune d'elle en une seule fois, nous pouvons définir la classe MacroCartCommand
suivante, qui est aussi une Commande :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
La classe MacroCartCommand
encapsule ainsi une collection d'actions, dont nous pouvons demander l'exécution par la même méthode d'interface CartCommand.execute
.
MacroCartCommand
possède aussi la méthode setReceiver
permettant de cibler le panier de notre choix.
Nous voyons que nous avons bien là un moyen de rejouer un ensemble d'actions sur un receveur donné.
Approche programmation fonctionnelle
Voyons maintenant comment nous pourrions implémenter notre exemple en ayant une approche du style programmation fonctionnelle, reconnaissant qu'une Commande peut s'implémenter au travers d'une Closure
Groovy : en effet, si une Commande s'exécute de manière différée, c'est aussi le cas d'une Closure
Groovy qui une fonction pouvant s'appeler plus tard.
Nous conservons les classes Item
et Cart
; par contre les commandes peuvent s'implémenter comme des fonctions d'ordre supérieur, sous la forme de fonctions renvoyant des Closure
s :
1 2 3 4 5 6 7 |
|
On peut ainsi créer la Commande souhaitée en passant à la fonction les paramètres nécessaires, qui retourne alors une Closure
, et invoquer cette dernière au moment opportun, comme ceci :
1 2 3 4 5 6 7 8 9 |
|
Dans cet exemple, addItem()
est bien sûr l'appel de la Closure retournée par makeAddItemCommand
.
Spécifier le receveur
Et si nous voulions pouvoir spécifier l'objet Cart
, le receveur, sur lequel appliquer la commande ?
Dans ce cas, nous pourrions modifier les fonctions makeAddItemCommand
et makeRemoveItemCommand
comme ci-dessous, en supprimant le paramètre cart
de chaque fonction, et en l'introduisant comme paramètre de la Closure retournée :
1 2 3 4 5 6 7 |
|
Cela signifie que pour invoquer maintenant une commande, il faut lui passer un objet Cart
:
1 2 3 4 5 6 7 8 9 10 |
|
En guise de conclusion
Nous voyons bien que l'approche fonctionnelle du modèle de conception Commande, mise en évidence avec le langage Groovy et ses particularités au travers d'un exemple, permet de réduire la "cérémonie" à mettre en place pour l'implémenter !