Whilst messing about with the memento pattern, I often found myself tripping over the command pattern. The patterns are similar enough that I confuse them, but dissimilar enough that I ought know better than to get them mixed up. So, at the risk of overloading on behavioral patterns, tonight I start on the command pattern in Dart for the forthcoming Design Patterns in Dart.
Something about the traditional light switch example for command patterns leaves me cold. That said, it is traditional for a reason—it uses physical object from the real world nicely in the example. So I start with that.
The idea behind the command pattern is the ability to represent a command as an object. That object can then be queued, replayed, or undone at a later time. The "command" in the pattern includes both a thing being acted on and an action being performed on that thing. In the traditional light switch example, the "thing" is a light and the action is turning the light on and off.
The
Light
class is a simple Dart class with a single method to turn the bulb on or off. In command pattern speak, the Light
is the receiver of the command:// Receiver class Light { void turn(String state) { print("Light ${state}"); } }The command itself has one requirement—that any command being run against the receiver executes some change. Thus the base class for commands requires that implementations define an
execute()
method:// Abstract command abstract class Command { void execute(); }With that, I can define concrete commands for the
Light
receiver:// Concrete Command class OnCommand implements Command { Light light; OnCommand(this.light); void execute() { light.turn('ON'); } } // Concrete Command class OffCommand implements Command { Light light; OffCommand(this.light); void execute() { light.turn('OFF'); } }Both commands require the
Light
receiver and both effect change on that receiver in their respective execute()
methods.With the core of the pattern out of the way, I am ready to focus on the "invoker." In the light switch example, the switch is the invoker, which is where this real-world example breaks down for me. I cannot think of a smart switch that might need to remember previous changes in state or a complicated switch that needs to queue commands. I am overthinking it, I know, so before I travel too far down that avenue, I simply define the
Switch
as:// Invoker class Switch { List<Command> _history = []; void storeAndExecute(Command c) { c.execute(); _history.add(c); } }The main purpose of the invoker is to execute the command. Here, it also stores the command for later undo.
That is the bulk of the pattern. The client code that uses the pattern needs an instance of the invoker and the receiver:
var s = new Switch(),
lamp = new Light();
The client also needs the commands that can be executed: var switchUp = new OnCommand(lamp),
switchDown = new OffCommand(lamp);
With that, the invoker can turn the light on at any time: s.storeAndExecute(switchUp);
And it can turn it off at any time: s.storeAndExecute(switchDown);
In this example, the power of the pattern is limited to the storage of the command. If the invoker defines an undo()
as:class Switch { List<Command> _history = []; // ... void undo() { _history. reversed. forEach((c) { c.execute(); }); } }Then previously executed switch states can be replayed in reverse order. There is more to the command pattern than undo. I will continue to explore that tomorrow.
Play with this code on DartPad: https://dartpad.dartlang.org/4530f9917cccbf8a2cbd.
Day #25
No comments:
Post a Comment