Il modello di comando in Java

1. Panoramica

Il modello di comando è un modello di progettazione comportamentale e fa parte dell'elenco formale di modelli di progettazione del GoF. In poche parole, il pattern intende incapsulare in un oggetto tutti i dati necessari per eseguire una data azione (comando), incluso quale metodo chiamare, gli argomenti del metodo e l'oggetto a cui appartiene il metodo.

Questo modello ci consente di disaccoppiare gli oggetti che producono i comandi dai loro consumatori , ecco perché il modello è comunemente noto come modello produttore-consumatore.

In questo tutorial, impareremo come implementare il modello di comando in Java utilizzando approcci sia orientati agli oggetti che funzionali agli oggetti, e vedremo in quali casi d'uso può essere utile.

2. Implementazione orientata agli oggetti

In un'implementazione classica, il modello di comando richiede l' implementazione di quattro componenti: il comando, il ricevitore, l'invocatore e il client .

Per capire come funziona il pattern e il ruolo che ogni componente gioca, creiamo un esempio di base.

Supponiamo di voler sviluppare un'applicazione per file di testo. In tal caso, dovremmo implementare tutte le funzionalità richieste per eseguire alcune operazioni relative ai file di testo, come l'apertura, la scrittura, il salvataggio di un file di testo e così via.

Quindi, dovremmo suddividere l'applicazione nei quattro componenti sopra menzionati.

2.1. Classi di comando

Un comando è un oggetto il cui ruolo è memorizzare tutte le informazioni richieste per eseguire un'azione , incluso il metodo da chiamare, gli argomenti del metodo e l'oggetto (noto come ricevitore) che implementa il metodo.

Per avere un'idea più precisa di come funzionano gli oggetti comando, iniziamo a sviluppare un semplice livello di comando che include solo una singola interfaccia e due implementazioni:

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

In questo caso, l' interfaccia TextFileOperation definisce l'API degli oggetti comando e le due implementazioni, OpenTextFileOperation e SaveTextFileOperation, eseguono le azioni concrete. Il primo apre un file di testo, mentre il secondo salva un file di testo.

È chiaro vedere la funzionalità di un oggetto comando: i comandi TextFileOperation racchiudono tutte le informazioni richieste per aprire e salvare un file di testo, incluso l'oggetto ricevitore, i metodi da chiamare e gli argomenti (in questo caso non sono richiesti argomenti, ma potrebbero essere).

Vale la pena sottolineare che il componente che esegue le operazioni sui file è il ricevitore (l' istanza TextFile ) .

2.2. La classe del ricevitore

Un ricevitore è un oggetto che esegue una serie di azioni coesive . È il componente che esegue l'azione effettiva quando viene chiamato il metodo execute () del comando .

In questo caso, dobbiamo definire una classe ricevente, il cui ruolo è modellare oggetti 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 invoker è un oggetto che sa come eseguire un determinato comando ma non sa come è stato implementato il comando. Conosce solo l'interfaccia del comando.

In alcuni casi, l'invoker memorizza e accoda anche i comandi, oltre a eseguirli. Ciò è utile per implementare alcune funzionalità aggiuntive, come la registrazione di macro o la funzionalità di annullamento e ripristino.

Nel nostro esempio, diventa evidente che deve esserci un componente aggiuntivo responsabile per invocare gli oggetti comando e eseguirli tramite il metodo execute () dei comandi . Questo è esattamente il punto in cui entra in gioco la classe invoker .

Diamo un'occhiata a un'implementazione di base del nostro invoker:

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

La classe TextFileOperationExecutor è solo un sottile strato di astrazione che separa gli oggetti comando dai loro consumatori e chiama il metodo incapsulato all'interno degli oggetti comando TextFileOperation .

In questo caso, la classe memorizza anche gli oggetti comando in un elenco . Ovviamente, questo non è obbligatorio nell'implementazione del pattern, a meno che non sia necessario aggiungere un ulteriore controllo al processo di esecuzione delle operazioni.

2.4. La classe client

Un client è un oggetto che controlla il processo di esecuzione dei comandi specificando quali comandi eseguire e in quali fasi del processo eseguirli.

Quindi, se vogliamo essere ortodossi con la definizione formale del pattern, dobbiamo creare una classe client utilizzando il tipico metodo main :

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. Implementazione funzionale dell'oggetto

Finora, abbiamo utilizzato un approccio orientato agli oggetti per implementare il modello di comando, che va benissimo.

Da Java 8, possiamo utilizzare un approccio funzionale agli oggetti, basato su espressioni lambda e riferimenti a metodi, per rendere il codice un po 'più compatto e meno dettagliato .

3.1. Utilizzo delle espressioni Lambda

Poiché l' interfaccia TextFileOperation è un'interfaccia funzionale, possiamo passare oggetti comando sotto forma di espressioni lambda all'invocatore , senza dover creare esplicitamente le istanze TextFileOperation :

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

In questo articolo, abbiamo appreso i concetti chiave del modello di comando e come implementare il modello in Java utilizzando un approccio orientato agli oggetti e una combinazione di espressioni lambda e riferimenti a metodi.

Come al solito, tutti gli esempi di codice mostrati in questo tutorial sono disponibili su GitHub.