Command

The idea of Command Pattern is to encapsulate a request in an object. We do that when we want other objects to have access to a number of requests in parametrical form. Besides, using this pattern we could also support undoable operations.

You could read a good description of this pattern, using Java as implementation language, on chapter six of Head First Design Patterns.

We typically define an interface, Command is its name in the pattern, that defines a couple of methods, execute() and undo(), that would be implemented by each ConcreteCommand.

A ConcreteCommand would define the execute() method calling one or more actions on another class (named Receiver in this context).

A Client is responsible for creating a ConcreteCommand and setting its Receiver.

An Invoker would keep a Command, and call its execute() method when required.

As an example of a situation where the Command Pattern could work well, we have a remote control emulation. We have seven slot in it, each of them could be associated with a different device, any slot is associated with a couple of buttons (on and off), each button would carry on a specific task. Besides we have an undo button that undoes the last pressed button.

In this case the Receiver would be the class implementing the features on which the remote control would actually work. For instance, we could have a class for the lights in the house that could be turned on and off:
public class Light {
   private String room;

   public Light(String room) {
      this.room = room;
   }

   public void on() {
      System.out.println(room + " lights are on");
   }

   public void off() {
      System.out.println(room + " lights are off");
   }
}
Here is the hierarchy of commands, with a couple of ConcreteCommands that refer to Light, our Receiver, and a Null Object, idiom described in the previous post:
public interface Command {
   public void execute();
   public void undo();
}

public class CommandLightOff implements Command {
   private Light light;

   public CommandLightOff(Light light) {
      this.light = light;
   }

   public void execute() {
      light.off();
   }

   public void undo() {
      light.on();
   }
}

public class CommandLightOn implements Command {
   private Light light;

   public CommandLightOn(Light light) {
      this.light = light;
   }

   public void execute() {
      light.on();
   }

   public void undo() {
      light.off();
   }
}

public class NoCommand implements Command {
   public void execute() {
      System.out.println("No command implemented");
   }

   public void undo() {
      System.out.println("No command implemented");
   }
}
The command interface declares that any ConcreteCommand should define how to execute and undo its own action.

And now it's time to implement the Invoker:
public class RemoteControl {
   public static enum Slot { ONE, TWO, THREE, FOUR, FIVE, SIX, SEVEN }
   private static int SLOT_NR = Slot.values().length; // 1

   private Command[] cmdOn;
   private Command[] cmdOff;
   Command last;

   public RemoteControl() { // 2
      cmdOn = new Command[SLOT_NR];
      cmdOff = new Command[SLOT_NR];

      Command noc = new NoCommand();
      for(int i = 0; i < SLOT_NR; ++i) {
         cmdOn[i] = noc;
         cmdOff[i] = noc;
      }
      last = noc;
   }

   public void setCommand(Slot slot, Command on, Command off) { // 3
      cmdOn[slot.ordinal()] = on;
      cmdOff[slot.ordinal()] = off;
   }

   public void pushOn(Slot slot) {
      last = cmdOn[slot.ordinal()];
      last.execute();
   }

   public void pushOff(Slot slot) {
      last = cmdOff[slot.ordinal()];
      last.execute();
   }

   public void pushUndo() {
      last.undo();
   }
}
1. I used enum to give the class a bit more robustness. The SLOT_NR constant is just a shorthand that makes the code a bit clearer. To change the number of slots available on the remote control is enough to define the name for a new slot in the enumeration.
2. The constructor creates the arrays for the on/off commands and initialize them using the Null Object. A better thought remote control would probably load its state from a configuration file.
3. We use setCommand() to define the action to be taken when an on/off button is pushed for a specific slot. To actually get the action related to the pushing of a button we have to call the pushOn()/pushOff() methods. To undo we have the pushUndo() method.

Finally, here is the client code that performs also a bit of testing:
RemoteControl rc = new RemoteControl();
rc.pushOn(RemoteControl.Slot.ONE);

Light lk = new Light("Kitchen");
Command klon = new CommandLightOn(lk);
Command kloff = new CommandLightOff(lk);

rc.setCommand(RemoteControl.Slot.ONE, klon, kloff);
rc.pushOn(RemoteControl.Slot.ONE);
rc.pushOff(RemoteControl.Slot.ONE);
rc.pushOn(RemoteControl.Slot.SEVEN);
rc.pushUndo();

Light lb = new Light("Bedroom");
Command blon = new CommandLightOn(lb);
Command bloff = new CommandLightOff(lb);
rc.setCommand(RemoteControl.Slot.SEVEN, blon, bloff);

for(RemoteControl.Slot cmd : RemoteControl.Slot.values())
   rc.pushOn(cmd);
rc.pushUndo();

Macro Command

We can extend our remote control to execute more action with a single click. The trick is based on this class:
public class MacroCommand implements Command {
   Command[] commands;

   public MacroCommand(Command[] commands) {
      this.commands = commands;
   }

   public void execute() {
      for(Command c : commands)
         c.execute();
   }

   public void undo() {
      for(Command c : commands)
         c.undo();
   }
}
The idea is quite simple: we construct the MacroCommand passing an array of commands. The MacroCommand.execute() would call execute() on each command in the array. And we apply the same strategy for undo().

To check the macro usage we add to our test client other few lines:
Command[] lightsOn = new Command[2];
lightsOn[0] = klon;
lightsOn[1] = blon;

Command[] lightsOff = new Command[2];
lightsOff[0] = kloff;
lightsOff[1] = bloff;

MacroCommand mcOn = new MacroCommand(lightsOn);
MacroCommand mcOff = new MacroCommand(lightsOff);

rc.setCommand(RemoteControl.Slot.SIX, mcOn, mcOff);
rc.pushOn(RemoteControl.Slot.SIX);
rc.pushUndo();
The call to pushOn() for the slot 6 would turn on the lights in both the registered rooms, and the undo would turn them off.

No comments:

Post a Comment