Le modèle de commande en Java

1. Vue d'ensemble

Le modèle de commande est un modèle de conception comportementale et fait partie de la liste formelle des modèles de conception du GoF. En termes simples, le modèle a l'intention d' encapsuler dans un objet toutes les données requises pour effectuer une action (commande) donnée, y compris la méthode à appeler, les arguments de la méthode et l'objet auquel appartient la méthode.

Ce modèle nous permet de découpler les objets qui produisent les commandes de leurs consommateurs , c'est pourquoi le modèle est communément appelé modèle producteur-consommateur.

Dans ce didacticiel, nous allons apprendre à implémenter le modèle de commande en Java en utilisant à la fois des approches orientées objet et fonctionnelles objet, et nous verrons dans quels cas d'utilisation cela peut être utile.

2. Mise en œuvre orientée objet

Dans une implémentation classique, le modèle de commande nécessite l' implémentation de quatre composants: la commande, le récepteur, l'invocateur et le client .

Pour comprendre le fonctionnement du modèle et le rôle joué par chaque composant, créons un exemple de base.

Supposons que nous souhaitons développer une application de fichier texte. Dans un tel cas, nous devons implémenter toutes les fonctionnalités requises pour effectuer certaines opérations liées aux fichiers texte, telles que l'ouverture, l'écriture, l'enregistrement d'un fichier texte, etc.

Nous devons donc décomposer l'application en quatre composants mentionnés ci-dessus.

2.1. Classes de commande

Une commande est un objet dont le rôle est de stocker toutes les informations nécessaires à l'exécution d'une action , y compris la méthode à appeler, les arguments de la méthode et l'objet (appelé récepteur) qui implémente la méthode.

Pour avoir une idée plus précise du fonctionnement des objets de commande, commençons à développer une couche de commande simple qui comprend une seule interface et deux implémentations:

@FunctionalInterface public interface TextFileOperation { String execute(); }
public class OpenTextFileOperation implements TextFileOperation { private TextFile textFile; // constructors @Override public String execute() { return textFile.open(); } }
public class SaveTextFileOperation implements TextFileOperation { // same field and constructor as above @Override public String execute() { return textFile.save(); } } 

Dans ce cas, l' interface TextFileOperation définit l'API des objets de commande, et les deux implémentations, OpenTextFileOperation et SaveTextFileOperation, exécutent les actions concrètes. Le premier ouvre un fichier texte, tandis que le second enregistre un fichier texte.

Il est clair de voir la fonctionnalité d'un objet de commande: les commandes TextFileOperation encapsulent toutes les informations requises pour ouvrir et enregistrer un fichier texte, y compris l'objet récepteur, les méthodes à appeler et les arguments (dans ce cas, aucun argument n'est requis, mais ils pourraient l'être).

Il convient de souligner que le composant qui effectue les opérations sur les fichiers est le récepteur (l' occurrence TextFile ) .

2.2. La classe des récepteurs

Un récepteur est un objet qui effectue un ensemble d'actions cohésives . C'est le composant qui exécute l'action réelle lorsque la méthode execute () de la commande est appelée.

Dans ce cas, nous devons définir une classe de récepteur, dont le rôle est de modéliser les objets TextFile :

public class TextFile { private String name; // constructor public String open() { return "Opening file " + name; } public String save() { return "Saving file " + name; } // additional text file methods (editing, writing, copying, pasting) } 

2.3. La classe Invoker

Un invocateur est un objet qui sait comment exécuter une commande donnée mais ne sait pas comment la commande a été implémentée. Il ne connaît que l'interface de la commande.

Dans certains cas, l'invocateur stocke et met également les commandes en file d'attente, en plus de les exécuter. Ceci est utile pour implémenter certaines fonctionnalités supplémentaires, telles que l'enregistrement de macros ou la fonctionnalité d'annulation et de rétablissement.

Dans notre exemple, il devient évident qu'il doit y avoir un composant supplémentaire chargé d'appeler les objets de commande et de les exécuter via la méthode execute () des commandes . C'est exactement là que la classe invocatrice entre en jeu .

Regardons une implémentation de base de notre invocateur:

public class TextFileOperationExecutor { private final List textFileOperations = new ArrayList(); public String executeOperation(TextFileOperation textFileOperation) { textFileOperations.add(textFileOperation); return textFileOperation.execute(); } }

La classe TextFileOperationExecutor est juste une fine couche d'abstraction qui dissocie les objets de commande de leurs consommateurs et appelle la méthode encapsulée dans les objets de commande TextFileOperation .

Dans ce cas, la classe stocke également les objets de commande dans une liste . Bien sûr, ce n'est pas obligatoire dans l'implémentation du modèle, à moins que nous ayons besoin d'ajouter un contrôle supplémentaire au processus d'exécution des opérations.

2.4. La classe client

Un client est un objet qui contrôle le processus d'exécution des commandes en spécifiant les commandes à exécuter et à quelles étapes du processus les exécuter.

So, if we want to be orthodox with the pattern's formal definition, we must create a client class by using the typical main method:

public static void main(String[] args) { TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); textFileOperationExecutor.executeOperation( new OpenTextFileOperation(new TextFile("file1.txt")))); textFileOperationExecutor.executeOperation( new SaveTextFileOperation(new TextFile("file2.txt")))); } 

3. Object-Functional Implementation

So far, we've used an object-oriented approach to implement the command pattern, which is all well and good.

From Java 8, we can use an object-functional approach, based on lambda expressions and method references, to make the code a little bit more compact and less verbose.

3.1. Using Lambda Expressions

As the TextFileOperation interface is a functional interface, we can pass command objects in the form of lambda expressions to the invoker, without having to create the TextFileOperation instances explicitly:

TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); textFileOperationExecutor.executeOperation(() -> "Opening file file1.txt"); textFileOperationExecutor.executeOperation(() -> "Saving file file1.txt"); 

The implementation now looks much more streamlined and concise, as we've reduced the amount of boilerplate code.

Even so, the question still stands: is this approach better, compared to the object-oriented one?

Well, that's tricky. If we assume that more compact code means better code in most cases, then indeed it is.

As a rule of thumb, we should evaluate on a per-use-case basis when to resort to lambda expressions.

3.2. Using Method References

Similarly, we can use method references for passing command objects to the invoker:

TextFileOperationExecutor textFileOperationExecutor = new TextFileOperationExecutor(); TextFile textFile = new TextFile("file1.txt"); textFileOperationExecutor.executeOperation(textFile::open); textFileOperationExecutor.executeOperation(textFile::save); 

In this case, the implementation is a little bit more verbose than the one that uses lambdas, as we still had to create the TextFile instances.

4. Conclusion

Dans cet article, nous avons appris les concepts clés du modèle de commande et comment implémenter le modèle en Java à l'aide d'une approche orientée objet et d'une combinaison d'expressions lambda et de références de méthode.

Comme d'habitude, tous les exemples de code présentés dans ce didacticiel sont disponibles à l'adresse over sur GitHub.