Das Befehlsmuster in Java

1. Übersicht

Das Befehlsmuster ist ein Verhaltensentwurfsmuster und Teil der formalen Liste der Entwurfsmuster des GoF. Einfach ausgedrückt, das Muster beabsichtigt, alle Daten, die zum Ausführen einer bestimmten Aktion (Befehl) erforderlich sind, in ein Objekt zu kapseln, einschließlich der aufzurufenden Methode, der Argumente der Methode und des Objekts, zu dem die Methode gehört.

Mit diesem Modell können wir Objekte entkoppeln, die die Befehle von ihren Verbrauchern erzeugen. Deshalb wird das Muster allgemein als Produzenten-Verbraucher-Muster bezeichnet.

In diesem Tutorial erfahren Sie, wie Sie das Befehlsmuster in Java implementieren, indem Sie sowohl objektorientierte als auch objektfunktionale Ansätze verwenden, und wir werden sehen, in welchen Anwendungsfällen dies nützlich sein kann.

2. Objektorientierte Implementierung

In einer klassischen Implementierung erfordert das Befehlsmuster die Implementierung von vier Komponenten: dem Befehl, dem Empfänger, dem Aufrufer und dem Client .

Um zu verstehen, wie das Muster funktioniert und welche Rolle jede Komponente spielt, erstellen wir ein grundlegendes Beispiel.

Nehmen wir an, wir möchten eine Textdateianwendung entwickeln. In einem solchen Fall sollten wir alle Funktionen implementieren, die zum Ausführen einiger textdateibezogener Vorgänge erforderlich sind, z. B. Öffnen, Schreiben, Speichern einer Textdatei usw.

Daher sollten wir die Anwendung in die vier oben genannten Komponenten aufteilen.

2.1. Befehlsklassen

Ein Befehl ist ein Objekt, dessen Aufgabe darin besteht, alle zum Ausführen einer Aktion erforderlichen Informationen zu speichern , einschließlich der aufzurufenden Methode, der Methodenargumente und des Objekts (als Empfänger bezeichnet), das die Methode implementiert.

Um eine genauere Vorstellung davon zu erhalten, wie Befehlsobjekte funktionieren, entwickeln wir zunächst eine einfache Befehlsebene, die nur eine einzige Schnittstelle und zwei Implementierungen enthält:

@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 diesem Fall definiert die TextFileOperation- Schnittstelle die API der Befehlsobjekte , und die beiden Implementierungen OpenTextFileOperation und SaveTextFileOperation führen die konkreten Aktionen aus. Ersteres öffnet eine Textdatei, während letzteres eine Textdatei speichert.

Die Funktionalität eines Befehlsobjekts ist klar erkennbar: Die TextFileOperation- Befehle enthalten alle Informationen, die zum Öffnen und Speichern einer Textdatei erforderlich sind , einschließlich des Empfängerobjekts, der aufzurufenden Methoden und der Argumente (in diesem Fall sind keine Argumente erforderlich). aber sie könnten sein).

Hervorzuheben ist, dass die Komponente, die die Dateivorgänge ausführt, der Empfänger ist (die TextFile- Instanz) .

2.2. Die Empfängerklasse

Ein Empfänger ist ein Objekt, das eine Reihe zusammenhängender Aktionen ausführt . Es ist die Komponente, die die eigentliche Aktion ausführt, wenn die execute () -Methode des Befehls aufgerufen wird.

In diesem Fall müssen wir eine Empfängerklasse definieren, deren Aufgabe es ist, TextFile- Objekte zu modellieren :

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. Die Invoker-Klasse

Ein Aufrufer ist ein Objekt, das weiß, wie ein bestimmter Befehl ausgeführt wird, aber nicht weiß, wie der Befehl implementiert wurde. Es kennt nur die Schnittstelle des Befehls.

In einigen Fällen speichert der Aufrufer neben der Ausführung auch Befehle und stellt sie in die Warteschlange. Dies ist nützlich, um einige zusätzliche Funktionen zu implementieren, z. B. Makroaufzeichnung oder Funktionen zum Rückgängigmachen und Wiederherstellen.

In unserem Beispiel wird deutlich, dass es eine zusätzliche Komponente geben muss, die dafür verantwortlich ist, die Befehlsobjekte aufzurufen und über die execute () -Methode der Befehle auszuführen . Genau hier kommt die Invoker-Klasse ins Spiel .

Schauen wir uns eine grundlegende Implementierung unseres Aufrufers an:

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

Die TextFileOperationExecutor- Klasse ist nur eine dünne Abstraktionsebene, die die Befehlsobjekte von ihren Verbrauchern entkoppelt und die in den TextFileOperation-Befehlsobjekten gekapselte Methode aufruft .

In diesem Fall speichert die Klasse auch die Befehlsobjekte in einer Liste . Dies ist natürlich in der Musterimplementierung nicht obligatorisch, es sei denn, wir müssen dem Ausführungsprozess der Operationen eine weitere Kontrolle hinzufügen.

2.4. Die Client-Klasse

Ein Client ist ein Objekt, das den Befehlsausführungsprozess steuert, indem es angibt, welche Befehle ausgeführt werden sollen und in welchen Phasen des Prozesses sie ausgeführt werden sollen.

Wenn wir also mit der formalen Definition des Musters orthodox sein möchten, müssen wir eine Client-Klasse mithilfe der typischen Hauptmethode erstellen :

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. Objektfunktionale Implementierung

Bisher haben wir einen objektorientierten Ansatz verwendet, um das Befehlsmuster zu implementieren, was alles gut und schön ist.

Ab Java 8 können wir einen objektfunktionalen Ansatz verwenden, der auf Lambda-Ausdrücken und Methodenreferenzen basiert, um den Code etwas kompakter und weniger ausführlich zu gestalten .

3.1. Verwenden von Lambda-Ausdrücken

Da die TextFileOperation- Schnittstelle eine funktionale Schnittstelle ist, können wir Befehlsobjekte in Form von Lambda-Ausdrücken an den Aufrufer übergeben , ohne die TextFileOperation- Instanzen explizit erstellen zu müssen :

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 diesem Artikel haben wir die Schlüsselkonzepte des Befehlsmusters und die Implementierung des Musters in Java mithilfe eines objektorientierten Ansatzes und einer Kombination aus Lambda-Ausdrücken und Methodenreferenzen kennengelernt.

Wie üblich sind alle in diesem Tutorial gezeigten Codebeispiele auf GitHub verfügbar.