O padrão de comando em Java
1. Visão geral
O padrão de comando é um padrão de design comportamental e faz parte da lista formal de padrões de design deGoF. Simplificando, o padrão pretende encapsulate in an object all the data required for performing a given action (command), incluindo qual método chamar, os argumentos do método e o objeto ao qual o método pertence.
Este modelo nos permitedecouple objects that produce the commands from their consumers, então é por isso que o padrão é comumente conhecido como o padrão produtor-consumidor.
Neste tutorial, aprenderemos como implementar o padrão de comando em Java usando abordagens orientadas a objetos e funcionais a objetos, e veremos em quais casos de uso ele pode ser útil.
2. Implementação Orientada a Objetos
Em uma implementação clássica, o padrão de comando requerimplementing four components: the Command, the Receiver, the Invoker, and the Client.
Para entender como o padrão funciona e a função que cada componente desempenha, vamos criar um exemplo básico.
Vamos supor que queremos desenvolver um aplicativo de arquivo de texto. Nesse caso, devemos implementar todas as funcionalidades necessárias para executar algumas operações relacionadas a arquivos de texto, como abrir, gravar, salvar um arquivo de texto e assim por diante.
Portanto, devemos dividir o aplicativo nos quatro componentes mencionados acima.
2.1. Classes de comando
Um comando é um objeto cuja função éstore all the information required for executing an action, incluindo o método a ser chamado, os argumentos do método e o objeto (conhecido como receptor) que implementa o método.
Para ter uma ideia mais precisa de como os objetos de comando funcionam, vamos começar a desenvolver uma camada de comando simples que inclui apenas uma única interface e duas implementações:
@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();
}
}
Neste caso, a interfaceTextFileOperation define a API dos objetos de comando, e as duas implementações,OpenTextFileOperationeSaveTextFileOperation, executam as ações concretas. O primeiro abre um arquivo de texto, enquanto o último salva um arquivo de texto.
É claro ver a funcionalidade de um objeto de comando: os comandosTextFileOperationencapsulate all the information required para abrir e salvar um arquivo de texto, incluindo o objeto receptor, os métodos a serem chamados e os argumentos (neste caso, não argumentos são necessários, mas podem ser).
Vale ressaltar quethe component that performs the file operations is the receiver (the TextFile instance).
2.2. A classe do receptor
Um receptor é um objeto queperforms a set of cohesive actions. É o componente que executa a ação real quando o métodoexecute() do comando é chamado.
Neste caso, precisamos definir uma classe receptora, cuja função é modelar objetosTextFile:
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. A classe Invoker
Um invocador é um objeto queknows how to execute a given command but doesn’t know how the command has been implemented. conhece apenas a interface do comando.
Em alguns casos, o invocador também armazena e enfileira comandos, além de executá-los. Isso é útil para implementar alguns recursos adicionais, como gravação de macro ou funcionalidade de desfazer e refazer.
Em nosso exemplo, fica evidente que deve haver um componente adicional responsável por invocar os objetos de comando e executá-los através do métodoexecute() dos comandos. This is exactly where the invoker class comes into play.
Vejamos uma implementação básica de nosso invocador:
public class TextFileOperationExecutor {
private final List textFileOperations
= new ArrayList<>();
public String executeOperation(TextFileOperation textFileOperation) {
textFileOperations.add(textFileOperation);
return textFileOperation.execute();
}
}
A classeTextFileOperationExecutor é apenas umthin layer of abstraction that decouples the command objects from their consumerse chama o método encapsulado nos objetos de comandoTextFileOperation.
Nesse caso, a classe também armazena os objetos de comando emList. Obviamente, isso não é obrigatório na implementação do padrão, a menos que seja necessário adicionar mais controle ao processo de execução das operações.
2.4. A Classe Cliente
Um cliente é um objeto quecontrols the command execution process especificando quais comandos executar e em quais estágios do processo executá-los.
Então, se quisermos ser ortodoxos com a definição formal do padrão, devemos criar uma classe de cliente usando o métodomain 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. Implementação funcional do objeto
Até agora, usamos uma abordagem orientada a objetos para implementar o padrão de comando, o que é muito bom.
No Java 8, podemos usar uma abordagem funcional de objeto, com base em expressões lambda e referências de método, paramake the code a little bit more compact and less verbose.
3.1. Usando Expressões Lambda
Como a interfaceTextFileOperation éfunctional interface, podemospass command objects in the form of lambda expressions to the invoker, sem ter que criar as instânciasTextFileOperation explicitamente:
TextFileOperationExecutor textFileOperationExecutor
= new TextFileOperationExecutor();
textFileOperationExecutor.executeOperation(() -> "Opening file file1.txt");
textFileOperationExecutor.executeOperation(() -> "Saving file file1.txt");
A implementação agora parece muito mais simplificada e concisa, pois temosreduced the amount of boilerplate code.
Mesmo assim, a questão ainda permanece: essa abordagem é melhor em comparação com a orientada a objetos?
Bem, isso é complicado. Se assumirmos que um código mais compacto significa melhor na maioria dos casos, então é verdade.
As a rule of thumb, we should evaluate on a per-use-case basis when to resort to lambda expressions.
3.2. Usando referências de método
Da mesma forma, podemos usar referências de método parapassing command objects to the invoker:
TextFileOperationExecutor textFileOperationExecutor
= new TextFileOperationExecutor();
TextFile textFile = new TextFile("file1.txt");
textFileOperationExecutor.executeOperation(textFile::open);
textFileOperationExecutor.executeOperation(textFile::save);
Nesse caso, a implementação éa little bit more verbose than the one that uses lambdas, pois ainda tivemos que criar as instânciasTextFile.
4. Conclusão
Neste artigo, aprendemos os principais conceitos do padrão de comando e como implementar o padrão em Java usando uma abordagem orientada a objetos e uma combinação de expressões lambda e referências de método.
Como de costume, todos os exemplos de código mostrados neste tutorial estão disponíveisover on GitHub.