Command (e Null Object)

Pattern descritto in Design Pattern.

Lo scopo del pattern Command (aka Action, Transaction) é incapsulare una richiesta in un oggetto, in questo modo si parametrizzano clienti con differenti richieste e si supportano operazioni reversibili.

Si usa Command:
  • per parametrizzare oggetti con una azione da eseguire. Nota che l'uso di Command in questo modo ricorda quello del meccanismo di callback, dove una funzione viene registrata per essere eseguita in seguito;
  • per specificare, mettere in coda, ed eseguire comandi successivamente;
  • per supportare l'undo di una operazione;
  • per permettere il log di cambiamenti in modo da poterli riapplicare in caso di bisogno. Aggiungendo i metodi load() e store() all'interfaccia si può tenere una traccia persistente dei cambiamenti;
  • per implementare le transazioni.
Esempio

Nel nostro mondo fantasy la potente corporazione dei maghi ha l'esclusiva sull'uso della magia. Ma c'é un modo per i non maghi di eseguire alcuni sortilegi: comprando una bacchetta magica su cui un mago ha registrato alcune magie ben delimitate.

Ad esempio si può chiedere a un mago di registrare l'incantamento che fa aprire una porta ben specificata e quello che la fa chiudere. E' prevista anche la possibilità di annullare la magia, nel caso la si sia invocata per sbaglio.

Alla base della nostra implementazione c'é l'interfaccia Command, che viene definita in questo modo:

package gof.command;

public interface Command {
public void execute();
public void undo();
}

E la classe Door, che rappresenta una porta, con i metodi che la fanno aprire e chiudere:

package gof.command;

public class Door {
private final String name;
private boolean open = false;

public Door(String name) {
this.name = name;
}

public String getName() {
return name;
}

public boolean isOpen() {
return open;
}

public void open() {
open = true;
}

public void close() {
open = false;
}

@Override
public String toString() {
return getName() + " is " + (isOpen() ? "open" : "closed");
}
}

La magia che ci permette di aprire una porta é descritta dalla classe OpenDoorSpell:

package gof.command;

public class OpenDoorSpell implements Command {
Door door;

public OpenDoorSpell(Door door) {
this.door = door;
}

public void execute() {
System.out.println(door.getName() + ": Open Sesame!");
door.open();
}

public void undo() {
System.out.println(door.getName() + ": Undo Open Sesame!");
door.close();
}
}

L'incantamento che ci permette di chiuderla é questo:

package gof.command;

public class CloseDoorSpell implements Command {
Door door;

public CloseDoorSpell(Door door) {
this.door = door;
}

public void execute() {
System.out.println(door.getName() + ": Close Sesame!");
door.close();
}

public void undo() {
System.out.println(door.getName() + ": Undo Close Sesame!");
door.open();
}
}

Vale la pena di crearsi anche una classe che rappresenta la non-magia. E' spesso utile avere un oggetto che non fa nulla, quando sappiamo a prescindere cosa fare nelle situazioni in cui non c'é un oggetto di quel tipo a disposizione. Nel caso delle magie é semplice: non succede nulla. L'uso di un oggetto che ha in sé la conoscenza di cosa fare in caso non vi sia alcun oggetto disponibile invece di lasciare all'utilizzatore il compito di decidere cosa fare in questa situazione viene descritto come il pattern del Null Object.

Null Object viene spesso implementato come un Singleton, come facciamo anche noi in questo caso:

package gof.command;

public class NullSpell implements Command {
private static NullSpell instance = new NullSpell();

public static NullSpell getInstance() {
return instance;
}

private NullSpell() {}

public void execute() {
System.out.println(this.getClass().getSimpleName() + ": no spell");
}

public void undo() {
System.out.println(this.getClass().getSimpleName() + ": nothing happen");
}
}

La nostra semplice bacchetta magica sarà un oggetto di questa classe:

package gof.command;

public class EasyWand {
public enum Type { ONE, TWO };
private static final int NR_SPELLS = 2;
private Command[] spells = new Command[NR_SPELLS];

public EasyWand() {
Command nothing = new NullSpell();

for(int i = 0; i < NR_SPELLS; ++i) {
spells[i] = nothing;
}
}

public void setSpell(Type type, Command spell) {
spells[type.ordinal()] = spell;
}

public void spell(Type type) {
spells[type.ordinal()].execute();
}
}

Notiamo che inizialmente le magie contenute nella bacchetta sono il Null Object. Come da aspettative, la bacchetta non fa nulla, bisogna esplicitamente associare magie alla bacchetta.

Test

Per prima cosa costruiamo una bacchetta secondo la richiesta del nostro utente, che vuole poter aprire e chiudere una porta di cui ci passa un riferimento:

private static void easyWand(Door door) {
Command open = new OpenDoorSpell(door);
Command close = new CloseDoorSpell(door);
EasyWand wand = new EasyWand();

System.out.println("A new wand, no spell on it");
wand.spell(EasyWand.Type.ONE);

wand.setSpell(EasyWand.Type.ONE, open);
wand.setSpell(EasyWand.Type.TWO, close);

System.out.println("The wand is ready to be used");
// ...

L'output di questa prima parte dovrebbe essere questo:

A new wand, no spell on it
NullSpell: no spell
The wand is ready to be used

Passiamo dunque al test vero e proprio della bacchetta. Apriamo, chiudiamo la porta, e poi cerchiamo due volte di fare l'undo. Siccome nella nostra implementazione é possibile fare l'undo solo dell'ultima operazione, il secondo undo non avrà successo:

// ...
System.out.println("Opening " + door.getName());
wand.spell(EasyWand.Type.ONE);
System.out.println(door.toString());

System.out.println("Closing " + door.getName());
wand.spell(EasyWand.Type.TWO);
System.out.println(door.toString());

System.out.println("Undoing Closing " + door.getName());
wand.undo();
System.out.println(door.toString());

wand.undo();
System.out.println(door.toString());
}

L'output atteso é il seguente:

Opening Front Door
Front Door: Open Sesame!
Front Door is open
Closing Front Door
Front Door: Close Sesame!
Front Door is closed
Undoing Closing Front Door
Front Door: Undo Close Sesame!
Front Door is open
NullSpell: nothing happen
Front Door is open

Nessun commento:

Posta un commento