El patrón de comando en Java

1. Información general

El patrón de comando es un patrón de diseño de comportamiento y es parte de la lista formal de patrones de diseño del GoF. En pocas palabras, el patrón intenta encapsular en un objeto todos los datos necesarios para realizar una acción determinada (comando), incluido el método a llamar, los argumentos del método y el objeto al que pertenece el método.

Este modelo nos permite desacoplar objetos que producen los comandos de sus consumidores , por eso el patrón se conoce comúnmente como patrón productor-consumidor.

En este tutorial, aprenderemos cómo implementar el patrón de comando en Java utilizando enfoques orientados a objetos y funcionales a objetos, y veremos en qué casos de uso puede ser útil.

2. Implementación orientada a objetos

En una implementación clásica, el patrón de comando requiere implementar cuatro componentes: el comando, el receptor, el invocador y el cliente .

Para comprender cómo funciona el patrón y el papel que desempeña cada componente, creemos un ejemplo básico.

Supongamos que queremos desarrollar una aplicación de archivo de texto. En tal caso, deberíamos implementar todas las funciones necesarias para realizar algunas operaciones relacionadas con archivos de texto, como abrir, escribir, guardar un archivo de texto, etc.

Entonces, deberíamos dividir la aplicación en los cuatro componentes mencionados anteriormente.

2.1. Clases de comando

Un comando es un objeto cuya función es almacenar toda la información necesaria para ejecutar una acción , incluido el método a llamar, los argumentos del método y el objeto (conocido como receptor) que implementa el método.

Para tener una idea más precisa de cómo funcionan los objetos de comando, comencemos a desarrollar una capa de comando simple que incluye una sola interfaz y dos implementaciones:

@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(); } } 

En este caso, la interfaz TextFileOperation define la API de los objetos de comando, y las dos implementaciones, OpenTextFileOperation y SaveTextFileOperation, realizan las acciones concretas. El primero abre un archivo de texto, mientras que el segundo guarda un archivo de texto.

Es claro ver la funcionalidad de un objeto de comando: los comandos TextFileOperation encapsulan toda la información requerida para abrir y guardar un archivo de texto, incluido el objeto receptor, los métodos a llamar y los argumentos (en este caso, no se requieren argumentos, pero podrían serlo).

Vale la pena enfatizar que el componente que realiza las operaciones del archivo es el receptor (la instancia de TextFile ) .

2.2. La clase del receptor

Un receptor es un objeto que realiza un conjunto de acciones cohesivas . Es el componente que realiza la acción real cuando se llama al método execute () del comando .

En este caso, necesitamos definir una clase receptora, cuya función es modelar objetos 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 clase de invocador

Un invocador es un objeto que sabe cómo ejecutar un comando dado, pero no sabe cómo se ha implementado el comando. Solo conoce la interfaz del comando.

En algunos casos, el invocador también almacena y pone en cola comandos, además de ejecutarlos. Esto es útil para implementar algunas funciones adicionales, como la grabación de macros o la funcionalidad de deshacer y rehacer.

En nuestro ejemplo, se hace evidente que debe haber un componente adicional responsable de invocar los objetos de comando y ejecutarlos a través del método execute () de los comandos . Aquí es exactamente donde entra en juego la clase de invocador .

Veamos una implementación básica de nuestro invocador:

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

La clase TextFileOperationExecutor es solo una capa delgada de abstracción que desacopla los objetos de comando de sus consumidores y llama al método encapsulado dentro de los objetos de comando TextFileOperation .

En este caso, la clase también almacena los objetos de comando en una lista . Por supuesto, esto no es obligatorio en la implementación del patrón, a menos que necesitemos agregar más control al proceso de ejecución de las operaciones.

2.4. La clase del cliente

Un cliente es un objeto que controla el proceso de ejecución de comandos especificando qué comandos ejecutar y en qué etapas del proceso ejecutarlos.

Entonces, si queremos ser ortodoxos con la definición formal del patrón, debemos crear una clase de cliente usando el método principal típico :

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. Implementación funcional de objetos

Hasta ahora, hemos utilizado un enfoque orientado a objetos para implementar el patrón de comando, lo cual está muy bien.

Desde Java 8, podemos usar un enfoque funcional de objetos, basado en expresiones lambda y referencias de métodos, para hacer que el código sea un poco más compacto y menos detallado .

3.1. Usar expresiones Lambda

Como la interfaz TextFileOperation es una interfaz funcional, podemos pasar objetos de comando en forma de expresiones lambda al invocador , sin tener que crear las instancias TextFileOperation explícitamente:

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

En este artículo, aprendimos los conceptos clave del patrón de comando y cómo implementar el patrón en Java mediante el uso de un enfoque orientado a objetos y una combinación de expresiones lambda y referencias a métodos.

Como de costumbre, todos los ejemplos de código que se muestran en este tutorial están disponibles en GitHub.