Das Command Design Pattern

Studienprojekt von Philipp Hauer. 2009 - 2010. ©

Inhalt

Einführung

Wir wollen Mitarbeiter modellieren. Dabei interessiert uns - im ersten Schritt - nur die Fähigkeit des Sekretärs, etwas ausdrucken zu können: Wird ihm gesagt, er soll etwas ausdrucken, so geht er zu dem firmeninternen Schwarz-Weiß-Drucker, nimmt Einstellungen vor und druckt das gewünschte Dokument aus.

Damit der Sekretär seinen Drucker auch kennt, geben wir ihm eine Referenz auf den SchwarzWeißDrucker.

Command Entwurfsmuster Einleitung

Code der Sekretär- und SchwarzWeissDrucker-Klasse:

class Sekretaer{
    private SchwarzWeissDrucker drucker;
    
    public void druckAusloesen(String dokument){
        drucker.konfigurieren();
        drucker.drucken(dokument);
    }
    
    //Andere Methoden...
    
}

class SchwarzWeissDrucker{
    public void konfigurieren(){
        //Einstellungen
    }
    public void drucken(String dokument){
        //Schwarz-Weiß-Druck
        System.out.println(dokument);
    }
}			
Client:

Sekretaer sekretaer = new Sekretaer();
sekretaer.druckAusloesen("Hallo!");			

Soweit so gut. Nun passiert jedoch etwas gänzlich Unerwartetes: Die Firma erwirbt einen modernen Farbdrucker. Allerdings dürfen nur ausgewählte Mitarbeiter in den Genuss des Farbdruckers kommen, wie der Vorstand. Um die entstandenen Anschaffungskosten zu kompensieren, wird gleichzeitig ein billiger Nadeldrucker angeschafft, damit der Schwarz-Weiß-Drucker entlastet wird. Praktikanten "dürfen" mit Nadeldruckern "arbeiten".

Command Design Pattern Einleitung

Nun, offensichtlich ist dies ein suboptimaler Entwurf. Warum?

  • Inflexibilität. Es wird klar, dass eine harte Zuordnung von Mitarbeiter und Drucker äußerst ungünstig ist. Was, wenn ein Sekretär plötzlich doch etwas farbig ausdrucken muss? Die Zuordnung zum Drucker kann nicht dynamisch zur Laufzeit erfolgen. Noch schlimmer: Er kann selbst zur Compilezeit nur mit mittelgroßen, destruktiven und irreversiblen Codeänderungen durchgeführt werden. Dies ist durch die enge Kopplung im Code bedingt: Mitarbeiter und Drucker sind auf ewig fest aneinander gekettet.
  • Keine Wiederverwendbarkeit. Befehle können nicht wiederverwendet werden. Soll nun der Werkstudent ebenfalls den Schwarz-Weiß-Drucker verwenden, so muss die Methode druckAusloesen() für den Schwarz-Weiß-Drucker im Werkstudent komplett neu implementiert werden. Es folgen Coderedundanzen (mehrmals die identische Bedinung eines speziellen Druckers implementieren) und damit die Gefahr von Inkosistenzen.

Das Strategy Design Pattern würde diesen Umstand versuchen aufzulösen, in dem es eine Druckerschnittstelle einführt. Den Mitarbeitern ist fortan nur noch die Druckerschnittstelle bekannt und sie delegieren ihren Druckaufruf an ihr Druckerobjekt. Über Getter und Setter wird der Drucker gesetzt.

Warum kann das nicht funktionieren?

  • Bei den Druckern handelt es sich nicht um ein Verhalten, das zu einem Mitarbeiter gehört, sondern um ein separates Objekt, an dem der Mitarbeiter seine Nachricht sendet.
  • Außerdem sind für das Drucken mehrere Arbeitsschritte je nach Zieldrucker notwendig (konfigurieren(), dann drucken()). Dazu kommt, dass diese Schritte druckerspezifisch sind (mal umstaendlichKonfigurieren(), mal nur konfigurieren(), mal gar nichts (oder speichern() statt drucken() beim PDFDrucker) und sich nicht in eine allgemeine Druckerschnittstelle abstrahieren lassen. Jeder Drucker muss anders behandelt werden.

Die Lösung liegt in der Einführung einer neuen Schicht zwischen Mitarbeiter und den Druckern: Der DruckBefehl. Dieser kapselt seinen Zieldrucker und weiß allein, welche druckerspezifischen Schritte notwendig sind.

Command Design Pattern Einleitung

Noch einmal: Allein der Befehl kennt seinen Empfänger (Drucker) und die durchzuführenden Schritte (konfigurieren(), drucken()). Der Mitarbeiter, unser Befehlsaufrufer, ist von diesem Wissen befreit. Er muss lediglich den gewünschten DruckBefehl anstoßen.

Command Desing Pattern Einleitung

Dieser Entwurf schreit förmlich nach der Einführung einer abstrakten Schnittstelle für die DruckBefehle (DruckBefehl). Die aufrufenden Mitarbeiter arbeiten folglich nur noch gegen diese Schnittstelle und delegieren die Druckarbeit an ihren Befehl. Sie wissen nicht, welcher konkrete Befehl sich dahinter verbirgt. Den Mitarbeitern ist es auch gleich, denn der Befehl weiß selbst, was mit wem zu tun ist.

Command Design Pattern Einführung

In der Zusammenschau ergibt sich folgendes Klassendiagramm.

Command Design Pattern Einleitung

DruckBefehlinterface und -implementierungen:

//Befehle
interface DruckBefehl{
    //Jeder Befehl kapselt die Logik zur Ausführung,
    //sowie den Zieldrucker
    public void ausfuehren(String dokument);
}
class SchwarzWeissDruckBefehl implements DruckBefehl{
    private SchwarzWeissDrucker drucker;
    
    //Der Befehl wird seinem Zieldrucker bei Instanziierung bekannt gemacht
    public SchwarzWeissDruckBefehl(SchwarzWeissDrucker drucker) {
        this.drucker = drucker;
    }

    public void ausfuehren(String dokument) {
        drucker.konfigurieren();
        drucker.drucken(dokument);
    }
}
class FarbDruckBefehl implements DruckBefehl{
    private FarbDrucker drucker;
    
    //Der Befehl wird seinem Zieldrucker bei Instanziierung bekannt gemacht
    public FarbDruckBefehl(FarbDrucker drucker) {
        this.drucker = drucker;
    }
    
    public void ausfuehren(String dokument) {
        drucker.drucken(dokument);
    }
}
class NadelDruckBefehl implements DruckBefehl{
    private NadelDrucker drucker;
    
    //Der Befehl wird seinem Zieldrucker bei Instanziierung bekannt gemacht
    public NadelDruckBefehl(NadelDrucker drucker) {
        this.drucker = drucker;
    }
    
    public void ausfuehren(String dokument) {
        drucker.umstaendlichKonfigurieren();
        drucker.drucken(dokument);
    }
}
class PDFDruckBefehl implements DruckBefehl{
    private PDFDrucker drucker;
    
    //Der Befehl wird seinem Zieldrucker bei Instanziierung bekannt gemacht
    public PDFDruckBefehl(PDFDrucker drucker) {
        this.drucker = drucker;
    }
    
    public void ausfuehren(String dokument) {
        drucker.speichern(dokument);
    }
}			
Die Aufrufer Sekretär, Vorstand etc.:

//Aufrufer
class Sekretaer{
    private DruckBefehl druckBefehl;
    
    //Aufrufer wird mit dem konkreten DruckBefehl über einen Setter geladen
    //Alternative: Konstruktor
    public void setDruckBefehl(DruckBefehl druckBefehl) {
        this.druckBefehl = druckBefehl;
    }

    public void druckAusloesen(String dokument){
        druckBefehl.ausfuehren(dokument);
    }
}
class Vorstand{
    private DruckBefehl druckBefehl;
    
    //Aufrufer wird mit dem konkreten DruckBefehl über einen Setter geladen
    //Alternative: Konstruktor
    public void setDruckBefehl(DruckBefehl druckBefehl) {
        this.druckBefehl = druckBefehl;
    }
    
    public void druckAusloesen(String dokument){
        druckBefehl.ausfuehren(dokument);
    }
}
//etc. identischer Code...
//Sinnvoll: Druckmembers in gemeinsame Superklasse Mitarbeiter auslagern			
Die Empfänger - Drucker:

//Empfänger
class SchwarzWeissDrucker{
    public void konfigurieren(){
        //Einstellungen
    }
    public void drucken(String dokument){
        //Schwarz-Weiß-Druck
        System.out.println(dokument);
    }
}
class FarbDrucker{
    public void drucken(String dokument){
        //Farb-Druck
        System.out.println(dokument.toUpperCase());
    }
}
class NadelDrucker{
    public void umstaendlichKonfigurieren(){
        //Einstellungen
    }
    public void drucken(String dokument){
        //Nadel-Druck
        System.out.println(dokument.toLowerCase());
    }
}
class PDFDrucker{
    public void speichern(String dokument){
        //Als PDF speichern
        System.out.println("PDF:"+dokument);
    }
}			

Jetzt bedarf es schließlich nur noch einer Instanz, die unser System initialisiert und konfiguriert. Danach kann das System genutzt werden.

Konfiguration und Nutzung durch den Client:

/*
 * Initiale Konfiguration:
 */
//Drucker erstellen
SchwarzWeissDrucker schwarzWeissDrucker = new SchwarzWeissDrucker();
FarbDrucker farbDrucker = new FarbDrucker();
NadelDrucker nadelDrucker = new NadelDrucker();
PDFDrucker pdfDrucker = new PDFDrucker();

Sekretaer sekretaer = new Sekretaer();
sekretaer.setDruckBefehl(new SchwarzWeissDruckBefehl(schwarzWeissDrucker));

/*
 * Nutzung
 */
sekretaer.druckAusloesen("Das Command Pattern ist super!");//Das Command Pattern ist super!

//ggf. dynamische Umkonfiguration zur Laufzeit
sekretaer.setDruckBefehl(new FarbDruckBefehl(farbDrucker));

sekretaer.druckAusloesen("Das Command Pattern ist super!");//DAS COMMAND PATTERN IST SUPER!			

Die erreichten Vorteile sind umwerfend:

  • Austauschbarkeit dank Modularität. Durch die Entkopplung von Aufrufer (Sekretär, Vorstand etc.) vom Zieldrucker sind die Befehle unabhängig von ihren Aufrufern und universell einsetzbar. Schnell kann jeder Mitarbeiter den Farbdrucker nutzen - sowohl statisch zur Compilezeit, als auch dynamisch zur Laufzeit -, es ist nur eine Zeile Code anzupassen. Die Befehle können beliebig wiederverwendet werden und neue Befehle sind schnell und problemlos integriert.
  • Das System ist nun in der Entwicklung fehlerresistenter. Konnte es vorher geschehen, dass ein Drucker unsachgemäß bedient wurde (konfigurieren() vergessen), so ist die Logik zur richtigen Bedienung der Drucker in einer einzigen Befehlsklasse enthalten.
  • Durch eben diese Zentralisierung der Befehlslogik wird Coderedundanz und Inkonsistenz vermieden.
  • Befehle können beliebige Empfänger haben und in beliebige Aufrufer geladen werden. Es bestehen keine Auflagen oder Vorschriften über Ausformung von Empfänger und Aufrufer. Ein Webservice könnte beispielsweise ins System eingebaut werden und fortan automatisiert den PDFDruckBefehl verwenden. Das System ist sehr flexibel.
  • Makro-Befehle. Da die Befehle entkoppelt sind, können sie beliebig zusammengefasst werden. Solch ein Makro-Befehl enthält andere Befehle in einer Liste. Wird der Makro-Befehl ausgeführt, so ruft er ausfuehren() auf allen Befehlen in der Liste aus.
    MakroBefehl DruckMitSicherheitskopieBefehl:
    
    class DruckMitSicherheitskopieBefehl implements DruckBefehl{
        private DruckBefehl[] druckBefehle = new DruckBefehl[3];
        //Konstruktor intialisiert die Befehle
        //FarbDruckBefehl, PDFDruckBefehl, SchwarzWeißDruckBefehl
    
        public void ausfuehren(String dokument) {
            for (DruckBefehl befehl : druckBefehle) {
                befehl.ausfuehren(dokument);
            }
        }
    }					
  • Beliebigkeit und Freiheit im Befehlsinhalt. Denkbar sind auch delegierende Befehle. So würde der Vorstand seine Dokumente drucken lassen.
    Dank Entkopplung können die Befehle beliebig ausgestaltet werden:
    
    class VorstandDruckBefehl implements DruckBefehl{
        private Sekretaer sekretaer;
        
        public VorstandDruckBefehl(Sekretaer sekretaer) {
            this.sekretaer = sekretaer;
        }
    
        //Der Vorstand lässt drucken! 
        public void ausfuehren(String dokument) {
            sekretaer.druckAusloesen(dokument);
        }
    }					

Hinweis: Diese Einführung behandelte nicht ein mächtiges Feature des Command Design Patterns: Die Rückgängig/Undo-Funktion, siehe dazu Variationen.

Es zeigt sich, dass durch den Einsatz des Command Patterns, ein hohes Maß an Flexibilität und Dynamik gewonnen wird, während zeitgleich die Wartung erleichtert wird und Erweiterungen schnell und unkompliziert möglich sind.

Nach dieser Einführung wird im folgenden Abschnitt das Command Design Pattern formalisiert, näher analysiert und diskutiert.

Das Druckerbeispiel mit Command Pattern Termini

Klasse Command Teilnehmer
Sekretär, Vorstand, Praktikant, Programmierer Aufrufer/Invoker
SchwarzWeissDrucker, Farbdrucker, Nadeldrucker, PDFDrucker Empfänger/Receiver
DruckBefehl Befehl/Command

Exkurs: Cyclomatic Complexity und Null-Objekt

Schauen wir uns folgenden Code von unserer Praktikanten-Klasse an.

Klasse Praktikant:

class Praktikant{
    private DruckBefehl druckBefehl;
    
    public void setDruckBefehl(DruckBefehl druckBefehl) {
        this.druckBefehl = druckBefehl;
    }
    
    public void druckAusloesen(String dokument){
        if (druckBefehl != null){
            druckBefehl.ausfuehren(dokument);
            //ggf. noch weitere Statements
            //...
        }
    }
}					

Betrachten wir die Methode druckAusloesen(): In ihr wird geprüft, ob das Field druckBefehl null ist, bevor der eigentliche Code ausgeführt wird. Damit wird eine NullpointerException verhindert. Dies geschieht allerdings auf Kosten der Lesbarkeit. Warum? Code ist dann für den Menschen gut zu erfassen, wenn er eine geringe Verschachtelungstiefe und -breite (if, else, switch, for, while, &&, || ) ausweist. Faustregel: So wenig wie möglich schachteln. Eine Software-Metrik, die diese Verschachtelungstiefe misst, ist die Cyclomatic Complexity. Sie beziffert die minimale Anzahl an möglichen Pfaden durch den Code.

Bei einer hohen Cyclomatic Complexity sollte sich gefragt werden, ob nicht Teile des Verschachtelungsbaums in sprechende Methoden ausgelagert werden können, um die Lesbarkeit, Übersichtlichkeit und Verständlichkeit zu sichern (Top-Down-Vorgehensweise).

Zurück zu unserer Klasse: Eine Lösung für das Verschachtelungsproblem ist denkbar simpel:

Code mit geringerer Cyclomatic Complexity:

public void druckAusloesen(String dokument){
    if (druckBefehl == null){
        return;
    }
    druckBefehl.ausfuehren(dokument);
    //ggf. noch weitere Statements
    //...
}					

Der objektorientierte Lösungsansatz dieses Problems kommt sogar ganz ohne Null-Check aus. Wir führen sogenannte Null-Objekte ein, die zwar das Interface DruckBefehl korrekt implementieren, jedoch nichts tun. Mit diesem Null-Objekt initialisieren wir unsere Instanzvariable.

Lösung mit Null-Objekt:

class Praktikant{
    //Initialisierung mit NullObjekt
    private DruckBefehl druckBefehl = new NullBefehl();
    
    public void setDruckBefehl(DruckBefehl druckBefehl) {
        this.druckBefehl = druckBefehl;
    }
    
    public void druckAusloesen(String dokument){
       druckBefehl.ausfuehren(dokument);
       //ggf. noch weitere Statements
       //...
    }
}

class NullBefehl implements DruckBefehl{
    public void ausfuehren(String dokument) {
        //Leer!
    }
}					

Unser Code wird wunderbar schlank und absturzsicher.

Darüber hinaus kann das Null-Objekt erweitert werden, sodass es zwar weiterhin nichts tut, aber seinen Aufruf loggt. Dadurch kann erkannt werden, wo im System eine Klasse uninitialisiert verwendet wird.

Analyse und Diskussion

Gang Of Four-Definition

Command:
"Kapsle einen Befehl als ein Objekt. Dies ermöglicht es, Klienten mit verschiedenen Anfragen zu parametrisieren, Operationen in eine Schlange zu stellen, ein Logbuch zu führen und Operationen rückgängig zu machen."
([GoF], Seite 273)

Beschreibung

Command Entwurfsmuster Beschreibung

Das Command Design Pattern ermöglicht die Modularisierung von Befehlen und Aufrufen. Auf elegante Weise können Befehle rückgängig gemacht, protokolliert oder in einer Warteschlange gelegt werden.

Invoker (Aufrufer) und Receiver (Empfänger) werden entkoppelt. Dazu werden die namensgebenden Command-Objekte (Befehle) zwischengeschaltet. Nur das Command-Objekt allein weiß, welche Aktionen auf welchem Empfänger auszuführen sind. So kennt ein SelectAllBefehl das gewünschte TextField und ruft selectAll(), die Aktion, auf diesem auf.

Dabei kennt der Aufrufer den Empfänger nicht - sie sind entkoppelt -, sondern nur die Command-Objekte. Genauer gesagt ein Commandinterface. Dadurch kann der Empfänger (beispielsweise eine Schaltfläche in einer GUI) beliebig mit Befehlen geladen werden oder bestehende können ausgetauscht werden.

Realisiert wird das Command Design Pattern wie folgt: Der Aufrufer hat ein oder mehrere Befehle (Commands), wobei er nur die Schnittstelle zu den Befehl kennt. Möchte er nun einen Befehl auslösen, so ruft er die execute()-Methode der Befehlsabstraktion auf. Der dahinterliegende konkrete Befehl, kennt seinen Empfänger (beispielsweise über eine Instanzvariable) und ruft nun die gewünschten Aktionen auf diesem auf (.action()). Er delegiert damit den Aufruf an den Empfänger.

Damit diese Interaktion stattfinden kann, muss vorher der Aufrufer mit den gewünschten Commands geladen werden. Dazu wird vorher ein konkretes Commandobjekt erstellt, und ihm ein Empfänger (Receiver) zugewiesen.

Command Design Pattern Beschreibung

Empfängerklassen:

//beliebige Empfänger
class ReceiverA{
    public void action1(){
        System.out.println("ReceiverA.action1()");
    }
    public void action2(){
        System.out.println("ReceiverA.action2()");
    }
    //weitere Methoden...
}
class ReceiverB{
    public void action1(){
        System.out.println("ReceiverB.action1()");   
    }
    //weitere Methoden...
}			
Commandinterface und -implementierungen:

//Befehlsinterface
interface Command {
    public void execute();
}

//Befehlsimplementierungen
class ConcreteCommandA implements Command {
    private ReceiverA receiverA;

    public ConcreteCommandA(ReceiverA receiver) {
        this.receiverA = receiver;
    }

    public void execute() {
        receiverA.action1();
        receiverA.action2();
    }
}

class ConcreteCommandB implements Command {
    private ReceiverB receiverB;

    public ConcreteCommandB(ReceiverB receiver) {
        this.receiverB = receiver;
    }

    public void execute() {
        receiverB.action1();
    }
}			
Aufruferklasse:

//dieser Invoker ist mit einer Reihe von Befehlen geladen
//je nach Methodenaufruf wird eins davon augelöst
class Invoker{
    private Command command1; //default möglich
    private Command command2; //default möglich
    
    //Eine Variante: Methode, die für Auführung des Befehls sorgt.
    //kann z. B. durch ein Event ausgelöst werden
    public void doCommand1(){
        command1.execute();
    }
    public void doCommand2(){
        command2.execute();
    }

    //Methode, mit der der Invoker konfiguriert werden kann
    //bzw. mit der die Commands geladen werden
    public void setCommand1(Command command1) {
        this.command1 = command1;
    }
    
    public void setCommand2(Command command2) {
        tnis.command2 = command2;
    }
}			

Variationen

Rückgängig- und Wiederherstellen-Funktion (Undo, Redo)

Das wohl mächtigste Feature des Command Design Patterns ist eine elegante Rückgängig-Funktion. Dafür wird die Schnittstelle des Commandinterfaces um eine undo()-Methode erweitert.

Rückgängig-Funktion beim Command Pattern

Diese Undo()-Methode enthält die exakte Umkehrung der execute()-Methode. Sorgt execute() dafür, dass eine Checkbox gesetzt wird, so deaktiviert undo() diese Checkbox wieder. In den meisten Fällen, muss sich das Befehlsobjekt die ursprünglichen Einstellungen (die während execute() überschrieben wurden) merken, um sie korrekt wiederherstellen zu können.

So oder so muss der Aufrufer ebenfalls um ein Befehlsgedächtnis erweitert werden. Im einfachsten Fall (Rückschrittfunktion mit einer Tiefe von 1) ist das ein zusätzliches Attribut im Aufrufer. Dieses wird bei jeder Befehlsausführung mit dem aktuellen Befehl gesetzt.

Einfache Rückschrittfunktion:

public class Invoker {
    private Command[] commands = new Command[5];
    //Letzten Befehl merken
    private Command lastCommand;
    
    public void undo(){
        if (lastCommand != null){
                lastCommand.undo();
                lastCommand = null;
        }
    }
    
    public void executeCommand(int index){
        commands[index].execute();
        lastCommand = commands[index];
    }
    
    //Methode, um Commands zu setzen....
}			

Denken wir diesen Gedanken konsequent zu Ende, so lassen sich mit Hilfe des Command Entwurfsmusters ganze Undo/Redo-Historys aufbauen. Der Aufrufer merkt sich in einer Liste (die History) alle bisher ausgeführten Befehle. Die Methoden undo() und execute()/redo() ermöglichen die Navigation in der Befehlshistoryliste. Um die letzten drei Befehle rückgängig zu machen, wird auf den letzten drei Befehlen in der Liste die undo()-Methode aufgerufen. Möchte man diese wiederum rückgängig machen (also Wiederherstellen), so wird in der Liste nach vorne navigiert und nochmals execute() auf den Befehlen aufgerufen, um die Änderungen wiederherzustellen.

Undohistory beim Command Design Pattern

Angedeuteter Invoker mit Commandhistory und Undo/Redo-Funktion:

public class Invoker2 {
    private Command[] commands = new Command[5];
    
    private List<Command> commandHistory = new ArrayList<Command>();
    private int lastCommandIndex = 0;
    
    public void undo(){
        commandHistory.get(lastCommandIndex--).undo();
    }
    public void redo(){
        commandHistory.get(++lastCommandIndex).execute();
    }
    
    public void executeCommand(int index){
        commands[index].execute();
        commandHistory.add(++lastCommandIndex, commands[index]);
        //ggf. alle Commands auf höheren Index als lastCommandIndex löschen...
    }
    
    //Methode, um Commands zu setzen....
}			

Es gilt: Je länger die Liste der CommandHistory ist, umso mehr Undo/Redo-Ebenen sind möglich.

Intelligenz der Commandobjekte

Grundsätzlich gibt es zwei Tendenzen im Funktionsumfang der Commands.

Intelligenz der Commands

  • Das Befehlsobjekt kann lediglich Bindeglied zwischen Aufrufer und Empfänger sein. Dabei enthält er keine eigenen Funktionen, sondern lediglich einen einfachen Methodenaufruf auf dem Empfänger.
    Der Befehl verbindet Aufrufer und Empfänger:
    
    public void execute(){
        datenschicht.refresh();
    }					
  • Das andere Extrem ist ein Commandobjekt, das die gesamte Funktionalität selbst implementiert und der Empfänger keine oder nur eine untergeordnete Rolle inne hat.

    Der Befehl beinhaltet die gesamte Logik:
    
    public void execute(){
        //fibonacci ausprinteln
        int zahl1 = 0;
        int zahl2 = 1;
        for (int i = 0; i < 100; i++) {
            System.out.println(zahl1);
            int temp = zahl1;
            zahl1 += zahl2;
            zahl2 = temp;
        }
    }					
    Der Vorteil ist hierbei, dass der Befehl unabhängig von einer bestehenden Klasse (dem Empfänger) wird. Er ist damit universell einsetzbar. Jedoch benötigt man in der Regel zwangsläufig einen Empfänger, da man Interaktionen zwischen Systemkomponenten (beispielsweise GUI (Aufrufer) und Rechenlogik(Empfänger)) herstellen möchte.

Makro-Befehle

Besonders interessant ist die Möglichkeit die modularen Befehle beliebig miteinander zu kombinieren und in einem neuen Befehlsobjekte zu kapseln. Man spricht hierbei von sogenannten Makro-Befehlen.

Makro-Befehl mit dem Command Design Pattern

Ein Makrobefehl hält eine Liste von Befehlen. Bei Ausführung (execute()) wird über die Befehlsliste iteriert und auf jedem Befehl execute() aufgerufen. Somit kann mit einem einzigen execute() eines MakroBefehls gleich mehrere Befehle auf einmal ausgeführt werden.

Anwendungsfälle

  • Grafische Benutzeroberflächen.
    • In komfortablen Benutzeroberflächen kann man durch verschiedene Schalter eine identische Funktion auslösen (Kontextklick, Schaltfläche, Menüpunkt, Hotkey). Statt die gewünschte Funktion für jeden Schalter erneut zu implementieren, können sie als Invoker fungieren und Befehlsobjekte aufnehmen, die sie beim entsprechendem User-Event ausführen. Damit lassen sich alle Schaltflächen mit einem einzigen Commandobjekt laden. Weiterhin können sie flexibel und problemlos ausgetauscht werden.
    • Wegen der herausragenden Eignung des Command Patterns zur Realisierung der Rückgängig-Funktion findet es in vielen Anwendungsprogrammen Verwendung.
  • Transaktionssysteme. Commandobjekte können in Fällen angewandt werden, in denen eine Reihe von Befehlen ausgeführt werden muss und es darauf ankommt, dass alle korrekt ausgeführt werden. Schlägt beispielsweise die letzte Aktion fehl, so können alle bis dahin durchgeführten Transaktionen zurückgerollt werden.
  • Command Design Pattern Progressbar/FortschrittsbalkenBestimmung von Prozessdauer für Fortschrittsbalken. Besteht ein Vorgang aus mehreren Befehlen, so kann die Commandschnittstelle um eine Methode getEstimatedDuration() erweitert werden. In dieser Methode gibt jeder Befehl seine geschätzte Abarbeitungszeit zurück. Die Gesamtdauer kann damit elegant und genau ermittelt werden.
    Anwendung Command Design Pattern
  • Makro-Recording und Scripting.
    • Viele Programme wie Photoshop oder FixFoto bieten die Möglichkeit, eine Folge von Benutzeraktionen aufzunehmen und als Makro abzuspeichern. Später kann diese Aktionsreihenfolge beliebig ausgeführt werden. Diese Funktion kann dank Command Entwurfsmuster sehr elegant implementiert werden. Es müssen lediglich alle während der Aufnahme ausgeführten Befehle in einer Liste gespeichert werden.
    • Auch ist eine Speicherung dieser Befehlsreihe in einer beliebigen Skriptsprache möglich. Dazu bedarf es wieder einer Erweiterung der Commandschnittstelle.
      Command Design Pattern Skricpting
      Jeder Befehl in der Liste kennt seine Entsprechung in der Skriptsprache und kann sie zurückgeben.
  • Wizards. Wizards automatisieren Benutzeraktionen. Jede Seite des Wizards ergibt ein oder mehrere Befehlsobjekte, die ebenfalls in einer Liste gespeichert werden können. Nach der letzten Seite, wird jeder Befehl der Liste ausgeführt. Durch die mehrfache Verwendung von Befehlsobjekten (durch normaler GUI, Wizards etc.) ergibt sich ein zentraler Punkt für Wartung und Änderungen.
  • Befehlswarteschlangen und Threadpools. Befehlsobjekten können ebenfalls in einer Warteschlange gesammelt werden. Wird ein Thread frei, nimmt er den Befehl am Kopf der Schlange ab und verarbeitet ihn. Sinnvoll ist die Verwendung eines gemeinsamen Interfaces für alle Befehle, damit die Threads sie verarbeiten können. Im Falle von Java wäre es java.lang.Runnable, wobei die run()-Methode den Verarbeitungscode des Befehls enthält. Der Thread (die Verarbeitungseinheit) ist damit vollkommen von der eigentlich auszuführenden Tätigkeit entkoppelt.

    Command Design Pattern Anwendung: Threadpool und Befehlswarteschlange (frei nach [VKBF], Seite 228)

  • Parallele Verarbeitung. Befehle können in einem eigenen Thread ausgeführt werden, sodass die Befehlsausführung im Hintergrund erfolgen kann.
  • Netzwerkversand. Befehlsobjekte können serialisiert und über das Netzwerk verschickt werden, um sie in anderen JVMs auszuführen. Auch ist eine Realisierung von Remotekontrolle von entfernen Systemen denkbar.
  • Protokollierung/Logging von Befehlen. Stürzt eine Applikation ab, so sind oft die ungespeicherten Änderungen verloren. Dem kann man entgegenwirken, in dem parallel zur Ausführung die Befehle ab dem letzten Speichern der Datei auf die Festplatte geschrieben werden (beispielsweise durch Serialisierung der Befehlsobjekte). Stürzt das System ab, kann nach einem Neustart die gespeicherten Befehle geladen und wieder auf der zuletzt gespeicherten Dateiversion ausgeführt werden. Eine Wiederherstellungsfunktion wurde damit implementiert.
    Command Pattern: Wiederherstellung

Vorteile

Im Zentrum des Command Design Patterns steht die Entkopplung von Aufrufer (Auslösen der Anfrage) und Empfänger (Ausführen der Anfrage) durch Befehlsobjekte. Die resultierenden Vorteile sind:

  • Modularität und Wiederverwendbarkeit von Befehlsobjekten. Befehle können von verschiedenen Aufrufern (Kontextklick, Schaltfläche, Menüpunkt, Hotkey) wiederverwendet werden.
  • Flexibilität und Dynamik.
    • Das Austauschen von Befehlen eines Aufrufers ist sehr einfach und kann ohne Brechen von weiterem Code durchgeführt werden.
    • Auch sind neue Befehle schnell erstellt (einfach Command realisieren und execute() implementieren) und ins System integriert. Wieder muss dazu kein Code angefasst werden.
    • Weiterhin können Befehle dynamisch zur Laufzeit gewechselt werden.
  • Kombination von Befehlen zu Makrobefehlen.
  • Einfache Implementierung von Rückgängig- oder Loggingfunktionalitäten.
  • Durch Reduzierung der Abhängigkeiten (mittels Delegation) wird die Kohäsion jedes Teilnehmers (Invoker, Command, Receiver) erhöht.
  • Vermeidung von Coderedundanz und Inkonsistenz durch zentrale Befehlsobjekte.
  • Befehlsobjekte können wie normale Objekte verwendet werden.
    • Erweiterung/Spezialisierung
    • Speichern, Laden, Versenden
    • Verändern, Filtern, Sortieren

Nachteile

  • Hohe Klassenanzahl. Da jeder Befehl eine neue Klasse bildet, kann es sehr schnell zu einer unüberschaubaren Vielfalt von konkreten Befehlen kommen. Die Schachtelung der modularen Befehle in Form von Makrobefehlen kann diesen Effekt bedingt verringern.

Anwendung in der Java Standardbibliothek

Swing: Action-Klasse (javax.swing.Action)

Swing Komponenten nutzen das Observer Design Pattern, um andere Klassen über Änderungen zu informieren (beispielsweise beim Drücken eines Buttons werden alle registrierten ActionListener informiert). Diese informierte Klasse weiß genau, was zu tun ist bzw. auf welchen Empfänger nun eine Aktion ausgeführt werden soll. Da auch in diesem Fall Aufrufer (Subjekt, Button) und Empfänger (Observer, ActionListener) voneinander entkoppelt sind, kann auch dies als eine Art Realisierung des Command Patterns betrachtet werden. Zumal auch selbe Observer/Commands für verschiedene Aufrufer/Subjekte genutzt werden können.

Aber Swing Komponenten unterstützen zum Eventhandling neben den Listener noch ein Konzept, das direkt dem Command Pattern entspricht: Action und AbstractAction.

Das Command Design Pattern in der Java Standardbibliothek, API

JButtons können mit einer Action (entspricht unserem Command) geladen werden:

JButton button1 = new JButton();
button.setAction(new MyAction());
//oder gleich im Konstruktor
JButton button2 = new JButton(new MyAction());			

So kann ein JButton und ein JMenuItem mit ein und derselben Action geladen werden. Eigentlich sind Actions nichts anderes als ActionListener (sie erweitern sogar dieses Interface) und werden intern vom JButton genauso gehandhabt. Sie werden ebenfalls in die ActionListenerListe aufgenommen.

Um eine eigene Action zu schreiben, kann AbstractAction erweitert werden. AbstractAction implementiert das Actioninterface:

class MyAction extends AbstractAction {

    public void actionPerformed(ActionEvent event) {
        System.out.println("MEINE AKTION!!");
    }
}			

Jedoch unterscheiden sich das Action- zum ActionListener-Konzept hinsichtlich folgender Aspekte:

  • Eine Komponente (JButton, JMenuItem, JComboBox, JTextField) kann immer nur mit einer Action geladen werden.
  • Actions bieten die Möglichkeit, den Zustand einer ActionEvent-feuernden Komponente durch Setzen der Action zu überschreiben. Der Zustand eines Buttons umfasst beispielsweise den Anzeigetext, Tooltip, Icons, Hotkey, Enabled- und Selectionstatus. Die gewünschte Konfiguration für diese Zustände können in einem Actionobjekte abgelegt und bequem auf verschiedenen Componenten (JButton, JMenuItem) übertragen werden.
    Konfiguration der Buttons mit einem Actionobjekt:
    
    Action startAction = new StartAction("Start", UIManager.getIcon("OptionPane.warningIcon"),
                    "This Button starts a prozess!", new Integer(KeyEvent.VK_L));
    JButton button = new JButton(startAction);
    JButton button2 = new JButton(startAction);
    JMenuItem menuItem = new JMenuItem(startAction);					
    Die geschriebene Actionklasse StartAction setzt Anzeigetext, Icon, Tooltip und Hotkey für den Button:
    
    class StartAction extends AbstractAction {
    
        public StartAction(String text, Icon icon, String desc, Integer mnemonic) {
            super(text, icon);
            putValue(SHORT_DESCRIPTION, desc);
            putValue(MNEMONIC_KEY, mnemonic);
        }
    
        public void actionPerformed(ActionEvent event) {
            System.out.println("huihu");
        }
    }				
    Das Codebeispiel ist dem Sun Tutorial "How to Use Actions" entnommen. Weiterführende Informationen sind dort zu finden.

java.lang.Runnable

Es wurde bereits angedeutet, dass das Command Pattern sich sehr gut für Multithreading eignet. Nun handelt es sich beim Interface Runnable um nichts anderes als um ein Commandinterface, das zudem direkt in einem eigenen Thread ausgeführt werden kann.

Command Pattern in java.lang.Runnable

Eine wie auch immer geartete Klasse des Clients fungiert als Invoker. Er stößt den Thread (und damit die Befehlsausführung) an, dem er eine Runnableimplementierung übergeben hat. Bei kluger Entkopplung kennt die Clientklasse, unser Invoker, nur die Runnableschnittstelle. Sie ist von der eigentlichen Tätigkeit der Runnable (bzw. des Threads) und dessen Empfänger entkoppelt.

Leider wird in der Praxis viel zu oft das volle Command-Potenzial nicht ausgeschöpft. Das liegt daran, dass die Clientklasse häufig nicht nur den Thread mit der Runnable abschickt (Command anstoßen), sondern auch die konkrete Runnableimplementierung selbst vorher instanziiert (Command laden). Da sie somit hart an eine Implementierung gekoppelt ist, kann die Clientklasse nicht dynamisch mit einer neuen Runnable geladen werden.

Ich möchte diese oft zweckmäßige Vorgehensweise nicht verteufeln, sondern nur auf das Potenzial des Runnable-Interfaces hinweisen.