Command (Design Pattern)

Aus Byte-Welt Wiki
Zur Navigation springenZur Suche springen

Das Command Design Pattern kapselt eine Anfrage als ein Objekt, so dass wir unsere Kunden mit unterschiedlichen Anfragen parametrieren können, Anfragen in einer Warteschlange oder einem Journal ablegen können, und aufhebbare Operationen unterstützen können.

Das Command Design Pattern entkoppelt ein Objekt das eine Anfrage macht, von demjenigen der weiß, wie diese Anfrage auszuführen ist. Im Mittelpunkt dieser Entkopplung steht ein Kommando-Objekt, das einen Empfänger und eine Aktion (oder eine Reihe von Aktionen) kapselt. Ein Aufrufer (invoker) richtet eine Anfrage an ein Kommando-Objekt durch den Aufruf seiner execute()-Methode, die wiederum die Aktionen des Empfängers (receiver) aufruft. Aufrufer können mit unterschiedlichen Befehlen konfiguriert werden, auch dynamisch zur Laufzeit.

Die Befehle können auch eine "undo"-Funktion unterstützen (rückgängig machen), indem sie eine undo()-Methode umsetzen, die das Empfänger-Objekt wieder in seinen vorherigen Zustand bringt, d.h. in den Zustand vor dem letzten Aufruf der Methode execute().

Ein Makro-Kommando ist eine einfache Erweiterung eines Kommandos, so dass er mehrere Befehle aufrufen kann. Ebenso kann ein Makro-Kommando die undo-Funktion leicht unterstützen.

Kommandos können auch für die Umsetzung von Transaktions- und Logging-Systemen verwendet werden.

Stellen wir uns zum Beispiel einen Empfänger "Prozessor" vor, der arithmetische Operationen durchführen kann:

package command;

public class Prozessor
{

   private double resultat;
   private String operator;
   private double operand;
   
   /**
    * Ein Prozessor für arithmetische Operationen
    */
   public Prozessor(){
   }

   /**
    * Berechnung von arithmetischen Operationen, wobei das Resultat kumuliert wird.
    * Das aktuelle Resultat wird auf dem Bildschirm angezeigt 
    * und kann mit Hilfe der Methode getResultat() abgefragt werden.
    * @param operator Addition=+, Substraktion=-, Multiplikation=* Division=/
    * @param operand eine Zahl vom Typ "double"
    */
   public void berechnung(final String operator, final double operand)
   {
      this.operator = operator;
      this.operand = operand;
      switch (operator)
      {
         case "+":
            resultat += operand;
            break;
         case "-":
            resultat -= operand;
            break;
         case "*":
            resultat *= operand;
            break;
         case "/":
            resultat /= operand;
            break;
      }
      System.out.println(this);
   }

   public double getResultat()
   {
      return resultat;
   }

   @Override
   public String toString()
   {
      return operator + " " + operand + " = " + resultat;
   }
}

Die Schnittstelle "Command" ermöglicht es, eine Aktion auf dem Prozessor auszuführen und rückgängig zu machen. Sie ist sehr einfach:

package command;

//Die Schnittstelle Command wird von den Klassen Berechnung, Reset und MacroCommand umgesetzt.
public interface Command
{

   public void execute();

   public void undo();
}

Die Klasse "Berechnung" ist ein konkreter Befehl, der eine arithmetische Operation berechnet:

package command;

//eine konkreter Befehl, der eine arithmetische Operation berechnet
public class Berechnung implements Command
{

   private Prozessor prozessor;
   private String operator;
   private double operand;

   public Berechnung(final Prozessor prozessor, final String operator, final double operand)
   {
      this.prozessor = prozessor;
      this.operator = operator;
      this.operand = operand;
   }

   @Override
   public void execute()
   {
      prozessor.berechnung(operator, operand);
   }

   @Override
   public void undo()
   {
      switch (operator)
      {
         case "+":
            prozessor.berechnung("-", operand);
            break;
         case "-":
            prozessor.berechnung("+", operand);
            break;
         case "*":
            prozessor.berechnung("/", operand);
            break;
         case "/":
            prozessor.berechnung("*", operand);
            break;
      }
   }

   @Override
   public String toString()
   {
      return operator + "" + operand;
   }
}

Die Klasse "Reset" ist auch ein konkreter Befehl, der den Prozessor zurücksetzt:

package command;

//ein konkreter Befehl, der den Prozessor zurücksetzt:
public class Reset implements Command
{

   private Prozessor prozessor;
   private double resultat;

   public Reset(final Prozessor prozessor)
   {
      this.prozessor = prozessor;
   }

   @Override
   public void execute()
   {
      resultat = prozessor.getResultat();
      prozessor.berechnung("-", resultat);
   }

   @Override
   public void undo()
   {
      prozessor.berechnung("+", resultat);
   }

   @Override
   public String toString()
   {
      return "Reset{" + resultat + '}';
   }
}

Die Klasse "MacroCommand" kann eine Reihe von Befehlen ausführen, die als Parameter übergeben werden:

package command;

import java.util.Arrays;

public class MacroCommand implements Command
{

   private final Command[] commands;

   /**
    * Ein MacroCommand kann eine Reihe von Befehlen ausführen, die als Parameter übergeben werden.
    *
    * @param commands eine Reihe von Befehlen
    */
   public MacroCommand(final Command[] commands)
   {
      this.commands = commands;
   }

   @Override
   public void execute()
   {
      for (int i = 0; i < commands.length; i++)
      {
         commands[i].execute();
      }
   }

   @Override
   public void undo()
   {
      for (int i = commands.length - 1; i >= 0; i--)
      {
         commands[i].undo();
      }
   }

   @Override
   public String toString()
   {
      return Arrays.toString(commands);
   }
}

Die Klasse "Taschenrechner" ist ein Aufrufer von Befehlen; sie weiß, wie man Befehle ausführen und rückgängig machen kann:

package command;

import java.util.ArrayList;
import java.util.List;

public class Taschenrechner
{

   private List<Command> undoable;
   private int undoLevel;

   /**
    * Ein Taschenrechner der Befehle ausführen und rückgängig machen kann.
    */
   public Taschenrechner()
   {
      undoable = new ArrayList<>(10);
      undoLevel = 0;
   }

   /**
    * Den Befehl ausführen, der als Parameter übergeben wird.
    *
    * @param command
    */
   public void execute(final Command command)
   {
      System.out.print("Berechnung: ");
      command.execute();
      addUndoable(command);
   }

   /**
    * Die als Parameter übergebene Anzahl von Befehlen rückgängig machen.
    *
    * @param number
    */
   public void undo(final int number)
   {
      System.out.println("undo(" + number + "):");
      for (int i = 0; i < number; i++)
      {
         if (undoLevel > 0)
         {
            Command commande = undoable.get(--undoLevel);
            commande.undo();
         }
      }
   }

   /**
    * Die als Parameter übergebene Anzahl von Befehlen wieder ausführen.
    *
    * @param number
    */
   public void redo(final int number)
   {
      System.out.println("redo(" + number + "):");
      for (int i = 0; i < number; i++)
      {
         if (undoLevel < undoable.size())
         {
            Command commande = undoable.get(undoLevel++);
            commande.execute();
         }
      }
   }

   //private Servicemethode:
   private void addUndoable(final Command command)
   {
      //die "undoable"-Liste dem aktuellen undoLevel anpassen:
      int count = undoable.size() - undoLevel;
      for (int i = 0; i < count; i++)
      {
         undoable.remove(undoable.size() - 1);
      }
      //den neuen Befehl hinzufügen:
      undoable.add(command);
      undoLevel = undoable.size();
   }
}

Und schließlich ist hier die Starter-Klasse, zum Testen unseres kleinen Taschenrechners, der dynamisch mit verschiedenen Befehlen konfigurierbar ist:

package command;

public class Demo
{

   public static void main(final String[] args)
   {
      Prozessor prozessor = new Prozessor();
      Taschenrechner taschenrechner = new Taschenrechner();
      
      taschenrechner.execute(new Berechnung(prozessor, "-", 4));
      taschenrechner.execute(new Berechnung(prozessor, "*", 3));
      taschenrechner.execute(new Berechnung(prozessor, "+", 14));
      double resultat = prozessor.getResultat();
      
      Command[] pi = new Command[]
      {
         new Reset(prozessor),
         new Berechnung(prozessor, "+", 103993),
         new Berechnung(prozessor, "/", 33102),
      };
      
      taschenrechner.execute(new MacroCommand(pi));
      taschenrechner.execute(new Berechnung(prozessor, "+", resultat));

      taschenrechner.undo(2);
      taschenrechner.redo(1);
      
      taschenrechner.execute(new Reset(prozessor));
   }
}

Der Taschenrechner gibt dies auf der Konsole aus:

Taschenrechner.jpg