Friday, February 26, 2010

The Command Pattern - Abstraction by Command

Ch. 6 from HFDP - Part 02 of 03

Target Audience: This series is pitched at developers interested in Object Oriented Design Patterns using the Moose framework in Perl. I cover the examples from the Head First Design Patterns book, replicating Java implementation in Moose. You need to obtain a copy of the book, which is not hard to come by.

So you want to replace your messy callback scripts with clean Command objects. Wise!

A Command implements an 'execute' or 'do' method that wraps around functionality implemented in the proletarian low-level API classes. We choose to call this wrapper-method 'execute'. Sticking with the Remote Control analogy, there is one command per button. So in case of an appliance such as a Light, there is one button for switching ON and another for switching OFF. One Command class for each operation.

[1.] Base Command Class
The base class has an undefined execute() method that is defined in the classes that inherit.
Let's lay out what we have set about doing. The Remote Control is the invoker - what the end-user sees and manipulates. This contrivance utilizes Command intermediaries that wrap around the meaty (but messy) methods in the devices - the Receivers. Why are the device methods messy? That is part of the plan. Because devices (i.e. Receivers) are of distinct types and the device manufacturers have the freedom of implementation. And that makes them messy to integrate in a Universal Remote Control. The problem is solved by using Command intermediaries that wrap a unified API around the device methods.

The 'execute' method is one element of this API. Can you figure out the other?

package Command;
use Moose;

use HouseholdDevices;

sub execute {};

sub undo {};


Right, 'undo' is the another. Like 'execute', it simply wraps around the appropriate behavior of the underlying Receiver (i.e. device).

[2.] Light On / Off Commands
Let's dive into a simple Command class starting with the Light class of devices.

Each Command class (on, off) has a slot to hold a light device - object of Light class. Implementation of execute() is straightforward. So is undo().

package LightOnCommand;
use Moose;

extends 'Command';

has 'light' => (
is => 'rw',
isa => 'Light',
);

sub execute {
print q{< Command id='LightOnCommand' >};
my $self = shift;
$self->light->on;
print q{< /Command >};
}

sub undo {
print q{< Command id='LightOnCommand' >};
my $self = shift;
$self->light->off;
print q{< /Command >};
}

package LightOffCommand;
use Moose;

extends 'Command';

has 'light' => (
is => 'rw',
isa => 'Light',
);

sub execute {
print q{< Command id='LightOffCommand >};
my $self = shift;
$self->light->off;
print q{< /Command >};
}

sub undo {
print q{< Command id='LightOffCommand >};
my $self = shift;
$self->light->on;
print q{< /Command >};
}


[3.] Light On / Off Commands - Living Room
More of the same. Just a different setting - the Living Room.

package LivingRoomLightOnCommand;
use Moose;

extends 'Command';

has 'light' => (
is => 'rw',
isa => 'Light',
);

sub execute {
print q{< Command id='LivingRoomLightOnCommand' >};
my $self = shift;
$self->light->on;
print q{< /Command >};
}

sub undo {
print q{< Command id='LivingRoomLightOnCommand' >};
my $self = shift;
$self->light->off;
print q{< /Command >};
}

package LivingRoomLightOffCommand;
use Moose;

extends 'Command';

has 'light' => (
is => 'rw',
isa => 'Light',
);

sub execute {
print q{< Command id='LivingRoomLightOffCommand >};
my $self = shift;
$self->light->off;
print q{< /Command >};
}

sub undo {
print q{< Command id='LivingRoomLightOffCommand >};
my $self = shift;
$self->light->on;
print q{< /Command >};
}


[4.] Fan Commands - On, off and everything in between
The fan presents an interesting case. It is a state machine - with four states as 'high', 'medium', 'low', 'off'. This means, four Command classes, one for each state, the execute() method being mapped accordingly. Implementation of the Commands for 'high' and 'off' states is reproduced below, the implementation of the remaining two states ('medium', 'low') following similar lines.

The slot 'backstate' is by virtue of the Ceiling Fan being a state machine. The previous state is saved - which happens every time a state-change operation (i.e. Fan Command) is executed. The undo() method is thus in business.

package CeilingFanHighCommand;
use Moose;
use Moose::Util::TypeConstraints;

extends 'Command';

has 'ceilingFan' => (
is => 'rw',
isa => 'CeilingFan',
);

enum 'fanState' => qw(high medium low off);

has 'backState' => (
is => 'rw',
isa => 'fanState',
default => 'off',
);

sub execute {
print q{};
my $self = shift;
$self->backState($self->ceilingFan->state);
$self->ceilingFan->high;
print q{
};
}

sub undo {
print q{};
my $self = shift;
$self->ceilingFan->low if ($self->backState eq 'low');
$self->ceilingFan->medium if ($self->backState eq 'medium');
$self->ceilingFan->off if ($self->backState eq 'off');
print q{
};
}

package CeilingFanOffCommand;
use Moose;
use Moose::Util::TypeConstraints;

extends 'Command';

has 'ceilingFan' => (
is => 'rw',
isa => 'CeilingFan',
);

enum 'fanState' => qw(high medium low off);

has 'backState' => (
is => 'rw',
isa => 'fanState',
default => 'off',
);

sub execute {
print q{};
my $self = shift;
$self->backState($self->ceilingFan->state);
$self->ceilingFan->off;
print q{
};
}

sub undo {
print q{};
my $self = shift;
$self->ceilingFan->high if ($self->backState eq 'high');
$self->ceilingFan->medium if ($self->backState eq 'medium');
$self->ceilingFan->low if ($self->backState eq 'low');
print q{
};
}


[5.] Fan Commands - with Roles
Notice anything questionable about the Commands for the Ceiling Fan? What is really dissatisfying is repetition of code. Analyzing the four Command classes, it is easily seen that the only change from class to class is in the implementation of the execute() method - the other slots and methods do not vary.

What can be done about it? In the first place, is it required to do anything? From the standpoint of the principles underlying object-oriented design, the implementation is really bad. The repetition of code across classes is symptomatic of design failure - we have failed to package code for reuse. Imagine if a new state is added to the Ceiling Fan class via Inheritance. For the new Ceiling Fan to work with the Fan Command classes, code has to be rewritten in four different places just to maintain the application. Not good!

And here is where Roles come in handy. Ever indulged in role-play? Nevermind, I just thought I'd ask ..

Let's visit the new implementation now. We already identified what bits of code vary and what stays the same in the implementation. I've moved the common bits to a Role called 'Fanable'.

package Fanable;
use Moose::Role;
use Moose::Util::TypeConstraints;

requires 'execute';

has 'ceilingFan' => (
is => 'rw',
isa => 'CeilingFan',
);

enum 'fanState' => qw(high medium low off);

has 'backState' => (
is => 'rw',
isa => 'fanState',
default => 'off',
);

before 'execute' => sub {
my $self = shift;
$self->backState($self->ceilingFan->state);
};

sub undo {
print q{};
my $self = shift;
$self->ceilingFan->high if ($self->backState eq 'high');
$self->ceilingFan->medium if ($self->backState eq 'medium');
$self->ceilingFan->low if ($self->backState eq 'low');
$self->ceilingFan->off if ($self->backState eq 'off');
print q{
};
}

Observe that a Role, like a class, may have slots for data as well as methods. But it is not a class in the sense that are no objects of a Role. A Role is composed into a class - a principle called 'mix-in' in some object-oriented circles.

What of 'requires'? It means that for a Class to consume a Role correctly, the method qualified by the 'requires' clause must be implemented therein. Otherwise, an exception is raised.

That means all Fan Command classes need to provide a defined execute() method.

Here then are the Commands for 'high' and 'off' states. See the difference?All it takes to consume the 'Fanable' Role is a 'with' clause. Define the execute() method .. and we're done! Ain't that neat?

package CeilingFanHighCommand;
use Moose;

with 'Fanable';

extends 'Command';

sub execute {
print q{};
my $self = shift;
$self->ceilingFan->high;
print q{
};
};

package CeilingFanOffCommand;
use Moose;

with 'Fanable';

extends 'Command';

sub execute {
print q{};
my $self = shift;
$self->ceilingFan->off;
print q{
};
};


[6.] Hot Tub
The Hot Tub serves as an excellent illustration of encapsulation. Here goes:

package HotTubOnCommand;
use Moose;

extends 'Command';

has 'hotTub' => (
is => 'rw',
isa => 'HotTub',
);

sub execute {
print q{};
my $self = shift;
$self->hotTub->on;
$self->hotTub->temperature(87);
$self->hotTub->circulate;
print q{
};
}

sub undo {
print q{};
my $self = shift;
$self->hotTub->off;
print q{
};
}

package HotTubOffCommand;
use Moose;

extends 'Command';

has 'hotTub' => (
is => 'rw',
isa => 'HotTub',
);

sub execute {
print q{};
my $self = shift;
$self->hotTub->temperature(35);
$self->hotTub->off;
print q{
};
}

sub undo {
print q{};
my $self = shift;
$self->hotTub->on;
print q{
};
}


[7.] No Command
I am skipping over the Television and Stereo. I never liked Televisions. Like Vincent Vega - from the Tarantino classic "Pulp Fiction", starring Uma Therman, John Travolta (Vincent) and Samuel Jackson (Jules).

Vincent: I don't watch TV.
Jules: Yeah, but, you are aware that there's an invention called television, and on this invention they show shows, right? ..

The only times when I've had a Television have been those when a girlfriend forced one upon me. Not that I am boring or anything. (I did love the Stereo though - A man that hath no music in himself is fit for treasons, spoils and strategems. A man that hath no stereo in his house is fit for iPods.)

What I shall move on to now is the No Command. In the Vedas, there is mention of 'shunya' - nothing. Nothing as in null. A non-entity. Not even an entity that stands for nothing of something, like zero. Much can go wrong if one ignores shunya. Civilizations have been known to crumble. Let's not make that mistake.

The No Command is what happens (or rather, what does not happen) when one presses a button on the Remote Control that isn't assigned (or rather, that is assigned to a No Command.) The idea is to leave no button unassigned - even when it does nothing, it does something that amounts to nothing. Here it is then, the No Command, which by now you know as well as the back of your hand.

package NoCommand;
use Moose;

extends 'Command';

sub execute {
print q{};
print q{
};
}

sub undo {
print q{};
print q{
};
}

That wasn't so tough, was it? ;o)

[8.] Macro Command
The last of the Commands is the Macro. It is a container for Commands, and yet, it is a Command itself. Remember Inheritance?

Before that starts to sounds scarier than it really is, here is the definition of the Macro.

package MacroCommand;
use Moose;

extends 'Command';

has 'Commands' => (
is => 'rw',
isa => 'ArrayRef[Command]',
default => sub { [] },
);

sub BUILD {
my $self = shift;
push @{$self->Commands}, NoCommand->new;
}

sub execute {
my $self = shift;
print q{};
foreach my $thisCommand (@{$self->Commands}) {
$thisCommand->execute;
}
print q{
};
}

sub undo {
my $self = shift;
print q{};
foreach my $thisCommand (@{$self->Commands}) {
$thisCommand->undo;
}
print q{
};
}

Note the use of BUILD to produce a valid initial state. Note also that the Macro is initialized with a No Command. The execute() method simply iterates over the array and invokes the execute() method of each Command. Isn't that neat? That's the power of abstraction - a container need not know anything about the Command objects contained in it. It simply iterates over those objects one by one, invoking the execute() method each time.

All code in this series may be downloaded from:
http://sites.google.com/site/sanjaybhatikar/codeunquote/designpatterns-1

No comments:

Post a Comment