Monday, May 24, 2010

The Command Pattern - SDLC

Ch. 6 from HFDP - Part 03 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.

How do you write code?

If you are like most software developers in a typical software shop, you write code the way you know to write code. Then you test it. Right? Or sometimes, you skip the tests. I mean, with all the constant pressure to put out code ..

How about a way to code which automatically generates tests and produces all the documentation you ever need? What is it worth to you? What if, this way also improves software quality and reduces the time duration for upgrades in the development life-cycle through the life of the software? What is it worth to you then?

There is a way, and like many other worthwhile things in life, all it requires is commitment to the process. The way consists of building an application starting with the tests, then write code to tests. On this side of the East-West divide, we say that all great truths are paradoxical. So writing tests and then coding to them is turning convention on the head. But it works. And you won't know until you do it. As Yoda tells the disbelieving young Jedi in "Empire Strikes Back" - Try not. Do. Or do not. There is not try.

It gets even better in a paired programming partnership, where one developer writes the tests and the other writes the code to those tests. Then, they switch roles. I have pair-programmed with programmers a level above me and a level below me. In both instances, I have improved my skills - learning new tricks from the better developers in our midst and from the questions of the less-skilled developers.

Mind you, paired programming is not a one-size fits all solution. There will always be lone gun-(wo)men. If you are in my position as the Head of a Software Development Group with a mix or people, personalities and cultures - it is best to open up paired-programming as an additional option that developers can easily avail of. And leave the choice to them. It helps to be flexible in these matters and keep an open mind.

Back to code. So what would the application test suite for the Remote Control look like? Let's start with the low-level APIs - the so-called Receiver classes. The code snippets illustrate sample tests for the Ceiling Fan and Hot Tub. First, the Fan, since it is kinda hot in here. (Or is it just me?)

print "--[ Receiver: Ceiling Fan ]--------------------------------------------";
my $myCeilingFan = CeilingFan->new;
isa_ok($myCeilingFan, 'CeilingFan');
$myCeilingFan->low;
is($myCeilingFan->state, 'low',
q{.. Set ceiling fan ON LOW});
$myCeilingFan->medium;
is($myCeilingFan->state, 'medium',
q{.. Set ceiling fan ON MED});
$myCeilingFan->high;
is($myCeilingFan->state, 'high',
q{.. Set ceiling fan ON HIGH});
$myCeilingFan->off;
is($myCeilingFan->state, 'off',
q{.. Set ceiling fan OFF});

This generates output like:


Now the 'ot tub:

print "--[ Receiver: Hot Tub ]------------------------------------------------";
my $myHotTub = HotTub->new;
isa_ok($myHotTub, 'HotTub');
$myHotTub->on;
is($myHotTub->state, 1,
q{.. Hot tub is on});
$myHotTub->off;
is($myHotTub->state, 0,
q{.. Hot tub is off});
$myHotTub->on;
$myHotTub->circulate;
$myHotTub->temperature(50);
is($myHotTub->temperature, 50,
q{.. Hot tub temperature is set to 50 degrees});
$myHotTub->temperature(150);
is($myHotTub->temperature, 150,
q{.. Hot tub temperature is set to 150 degrees});
$myHotTub->temperature(90);
is($myHotTub->temperature, 90,
q{.. Hot tub temperature is set to 90 degrees});

Which generates the output:


Quite a screenful, eh? Nothing too sophisticated though. All we are doing is testing class labels with 'isa_ok' and get/set (i.e. accessor/mutator) methods with 'is'. So what's the big deal?

Let's examine the output.

First, note how the documentation is implicit. The 'isa_ok' tests provide a good opportunity to explain the constructor calls. The 'is' tests present an opportunity for one-liners as succinct documentation of the get/set methods - at least the important ones. Any other methods can be tested - and documented - in the same manner.

Note that these tests are based on Perl's utilitarian 'Test::More' package. (The 'use' declaration is not shown in the snippets posted above. Refer to the code for these details.) Some handy utilities from the 'Test::More' package are 'like' for tests based on a pattern match, 'can_ok' to test if an object or class has a method, 'is_deeply' for validating data structures with XML-style nested organization.

The Command APIs are similarly tested. Take a look at the Ceiling Fan - being a state machine, its tests are interesting!

print "--[ Command: Ceiling Fan Hi / Lo / Med / Off]--------------------------";
my $myTestCeilingFan = CeilingFan->new;
print "-----------------------------------------------------------< ON HI >---";
$myTestCeilingFan->medium; # fan set randomly at medium
is($myTestCeilingFan->state, 'medium',
q{Fan is ON MED at start.});
my $myCeilingFanHighCommand =
CeilingFanHighCommand->new(ceilingFan => $myTestCeilingFan);
isa_ok($myCeilingFanHighCommand, 'CeilingFanHighCommand');
$myCeilingFanHighCommand->execute;
is($myCeilingFanHighCommand->ceilingFan->state, 'high',
q{Fan (re)set ON HI via Command.});
$myCeilingFanHighCommand->undo;
is($myCeilingFanHighCommand->ceilingFan->state, 'medium',
q{Fan (re)set ON MED via undo.});
print "----------------------------------------------------------< ON MED >---";
$myTestCeilingFan->low; # fan set randomly at low
is($myTestCeilingFan->state, 'low',
q{Fan is ON LO at start.});
my $myCeilingFanMediumCommand =
CeilingFanMediumCommand->new(ceilingFan => $myTestCeilingFan);
isa_ok($myCeilingFanMediumCommand, 'CeilingFanMediumCommand');
$myCeilingFanMediumCommand->execute;
is($myCeilingFanMediumCommand->ceilingFan->state, 'medium',
q{Fan (re)set ON MED via Command.});
$myCeilingFanMediumCommand->undo;
is($myCeilingFanMediumCommand->ceilingFan->state, 'low',
q{Fan (re)set ON LO via undo.});
print "----------------------------------------------------------< ON LO >---";
$myTestCeilingFan->high; # fan set randomly at high
is($myTestCeilingFan->state, 'high',
q{Fan is ON HI at start.});
my $myCeilingFanLowCommand =
CeilingFanLowCommand->new(ceilingFan => $myTestCeilingFan);
isa_ok($myCeilingFanLowCommand, 'CeilingFanLowCommand');
$myCeilingFanLowCommand->execute;
is($myCeilingFanLowCommand->ceilingFan->state, 'low',
q{Fan (re)set ON LO via Command.});
$myCeilingFanLowCommand->undo;
is($myCeilingFanLowCommand->ceilingFan->state, 'high',
q{Fan (re)set ON HI via undo.});
print "-------------------------------------------------------------< OFF >---";
$myTestCeilingFan->high; # fan set randomly at high
is($myTestCeilingFan->state, 'high',
q{Fan is ON HI at start.});
my $myCeilingFanOffCommand =
CeilingFanOffCommand->new(ceilingFan => $myTestCeilingFan);
isa_ok($myCeilingFanOffCommand, 'CeilingFanOffCommand');
$myCeilingFanOffCommand->execute;
is($myCeilingFanLowCommand->ceilingFan->state, 'off',
q{Fan (re)set OFF via Command.});
$myCeilingFanOffCommand->undo;
is($myCeilingFanLowCommand->ceilingFan->state, 'high',
q{Fan (re)set ON HI via undo.});

Notice how each Command is given a starting state that is changed upon invocation of the 'execute' method, and restored with 'undo'? Nifty!



One can continue on in this manner. For this example, the Invoker (i.e Remote) is tested by means of a dry-run. See the code for details.

Epilogue:
Unit-testing in this way looks tedious and time-consuming. Yes, it can be that. Particularly if code is written first and tests later. That's why it is very, very, very, very, very, very, very, very, very, very, very important to write the tests first. Web-Application Frameworks in Perl such as Catalyst are designed with this approach in mind.

Writing tests first gives rise to lots of questions. That's because the coder is likely to ask the tester about the architectural features of the system as he/she gets their mind around the design. Perhaps there are some inconsistencies of the architecture that come to light. This way, there is a far greater chance that difficulties are noticed early, before a lot of code is written. That can be majorly beneficial.

Yet another benefit is that there is always a benchmark for changes made during the life-cycle of the application. The test-suite, which is continually updated, serves as a basis for regression testing.

For example, while writing this little application, I decided to upgrade the Remote Control class implementation with traits - a feature of Moose 1.05+. After working this into the code, I immediately ran the application test suite to ensure nothing was broken. Sure enough, things were mis-shapen. But with regression testing to guide the development process, it the process was far easier.

Always built testing into your process. It is key to developing an application incrementally, in small and consistent steps. Now that's bringing Kaizen to the software world.

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

No comments:

Post a Comment