A Handy Builder Pattern
I'm working on a web service, and that means that I need to build lots and
lots of mildly different looking HTTP requests with various combinations of
headers and requested URLs. The camel's back got broken this morning when I
realised I didn't want to be writing a method called
ssl_request_from_uk_with_bad_cert, which builds me an HTTP::Request with a
particular combination of headers, that I can use with Plack::Test to
test our webservice. The method name describes what's wanted, but the code is
sopping wet and in desperate need of DRYing up.
My first cut was to write a simple request builder:
package RequestBuilder;
use Moose;
with 'MooseX::OneArgNew' => {
type => 'HTTP::Request',
init_arg => '_req',
}
has _req => (
is => 'ro',
isa => 'HTTP::Request',
handles => [qw(header headers)],
);
sub ssl_request {
my $class = shift;
$class->new(HTTP::Request->new(
GET => 'https://localhost/',
[
SSLSessionID => 'deadbeef',
],
);
}
sub from_uk {
my $self = shift;
$self->header('X-IP-Is-UK-Combined' => 'yes');
return $self;
}
sub with_bad_cert {
my $self = shift;
$self->header('SSLClientCertStatus' => 'NoClientCert');
return $self;
}
sub final {
my $self = shift;
$self->_req;
}
Now I have a builder, I can compose meaningful fragments to build a final request. So I might write:
my $r = RequestBuilder->ssl_request
->from_uk
->with_bad_cert
->final;
But things got sticky when I wanted to to take a valid request and bend it out of shape to ensure that it failed correctly. I was writing things like:
use RequestBuilder;
sub valid_request {
my($self, $is_final) = @_;
my $r = RequestBuilder->ssl_request->from_uk;
return $is_final ? $r->final : $r;
}
test "we reject requests from outside the UK" => sub {
my $self = shift;
my $r = $self->valid_request(0)->from('Turkmenistan')->final;
...
};
which works, but is uglier than a very ugly thing indeed. What I wanted was some way of having the finalization magically happen at the point of use but have some way of extending any intermediate results. The interface I came up with looks like this:
use RequestBuilder;
sub valid_request {
build_request {
$_->ssl_request->from_uk;
}
}
test "we reject requests from outside the UK" => sub {
my $self = shift;
my $r = build_request {
$self->valid_request->from('Turkemenistan');
};
...
};
When the builder is returned from the outermost build_request block, it gets
finalized.
"How does that work then?" I hear you ask. It's quite simple, once you know about Moose::Exporter, old fashioned perl prototypes and the Tao of dynamic scope. We just add something like the following to the end of our RequestBuilder:
our $building;
use Moose::Exporter;
Moose::Exporter->setup_import_methods(
as_is => ['build_request'],
);
sub build_request (&) {
my $block = shift;
my $builder = do {
local $building = 1;
local $_ = __PACKAGE__;
$block->();
};
return $building ? $builder : $builder->_req;
}
So, build_request has a & prototype, which means it takes a block as its
first argument. Perl treats an initial prototyped block argument as slightly magical and
doesn't require the use of the sub keyword (though you can use it if you
want). It then uses a do block to dynamically set $building to 1 and $_
to be the current __PACKAGE__ name (because $_ is shorter to type
than RequestBuilder) before calling the block to get a builder. Then it
checks whether we're still building. If we are, it returns the builder. If we
aren't, then the block was the outermost block, so build_request returns the
built request.
I'm not quite ready to extract this pattern into a parameterized role - I need to make sure it's robust enough, but it's certainly something to think about when you next need to make a builder for your tests.
Class decomposition and a handy delegation pattern 3
There’s something satisfying about reaching the point when you can’t decompose an object any further and all your methods are tiny and do one thing – it’s especially gratifying when you learn something new in the process. Sadly, it doesn’t happen as often as I’d like, there’s usually annoying bits and pieces where you have to placate the language in some fashion that breaks the flow of what you’re writing.
As I get a better handle on the way MooseX::Declare has changed Perl, I’m finding I have to do much less in the way of placation.
Here’s an example. For context, I’m writing a traffic shaping tool. The basic client interface needs to look something like:
$policy->current_weight # => a percentage between 0 and 100
Not much of an interface really. The requirements state that we should be able to specify weights with 15 minute granularity for every day of the week. Our problem becomes one of mapping from a time to a number between 0 and 7 (days) * 24 (hours) * 4 (quarters) - 1 and looking up the weight in an array.
Here’s my first cut:
use MooseX::Declare;
class WeightVector {
use DateTime;
has vector => (
isa => 'Array[Int]',
is => 'ro',
required => 1,
);
method current_weight {
my $now = DateTime->now;
my $offset = ($now->wday_0 * 7 * 24 + $now->hours) * 4 + int($now->minutes / 15);
return $self->vector->[$offset];
}
}Which is, I suppose, perfectly respectable. However, current_weight isn’t filling me with delight. First it finds the current time, then it converts the time into an offset, then it uses the offset to lookup the weight in the vector. Let’s introduce a method to find the weight at a specific time"1":#decomp-motivation, the relevant code becomes:
method current_weight {
$self->weight_at(DateTime->now);
}
method weight_at (DateTime $time) {
my $offset = ($now->wday_0 * 7 * 24 + $now->hours) * 4 + int($now->minutes / 15);
return $self->vector->[$offset];
}And again, we could rest here, but again, we’re doing two things. We’re converting from a time to an offset, then we’re looking up the value in the vector. Type conversions tend to happen again and again, so it’s good if we can specify them separately. We could write a time_to_offset helper method, but we’re in Mooseland now; there’s a better way. Let’s introduce a formal Moose type and define a coercion for it. Here’s the type definition stanza of the code. I’ve taken the opportunity to add types which do bounds checking for the vector as well, while I’m about it.2
use MooseX::Declare;
class WeightVector {
use DateTime;
use Moose::Autobox;
use MooseX::Types -declare => [qw(SlotOffset VectorOfWeights PercentageInt)];
use MooseX::Types::Moose qw(ArrayRef Int);
use constant TOTAL_SLOTS = 7 * 24 * 4;
BEGIN {
subtype PercentageInt,
as Int,
where { 0 <= $_ && $_ <= 100 },
message { "$_ does not is not an integeter between 0 and 100" };
subtype VectorOfWeights,
as ArrayRef[PercentageInt],
where { $_->length == TOTAL_SLOTS }
message { "Vector must have ".TOTAL_SLOTS." entries, not ".$_->length };
subtype SlotOffset,
as Int,
where { 0 <= $_ && $_ < TOTAL_SLOTS };
class_type 'DateTime';
coerce SlotOffset,
from 'DateTime',
via { ($_->wday_0 * 7 * 24 + $_->hours) * 4 + int($_->minutes / 15) };
# Let's allow clients not to care about using DateTime by allowing
# them to simply pass the results of calling 'time()' - It's not like it's
# still 1970...
coerce SlotOffset
from subtype(as => Int, where { $_ > TOTAL_SLOTS }
via { to_SlotOffset(DateTime->from_epoch(epoch => $_) };
}
has vector => (
is => 'ro',
isa => VectorOfWeights,
required => 1,
}
...Now, if we were programming in plain old Moose, we could rewrite weight_at like so:
sub weight_at {
my $self = shift;
my $offset = to_SlotOffset(shift);
$self->vector->[$offset]
}Which would be pretty sweet, but we’re using MooseX::Declare; there’s an even better way:
method weight_at (SlotOffset $offset does coerce) {
$self->vector->[$offset];
}Sweet!
We could stop there, but I had an insight. What we’ve got here is basically a wrapper around a delegation to our vector, and Moose’s new native types feature let us express the delegation to the vector quite neatly, like so:
has vector => (
isa => 'VectorOfWeights',
is => 'ro',
required => 1,
traits => ['Array'],
handles {
weight_at => 'get',
},
);
...
around weight_at (SlotOffset $offset does coerce) {
$self->$orig($offset);
}This could be overkill when vector is a simple ArrayRef as we have here, but the pattern of delegating declaratively in the attribute definition and then munging arguments in an around handler is applicable to more than just argument transformation. A typical delegation pattern involves having the delegating object passing itself in as an argument to the method delegated to. The nature of Moose’s handles declarations makes that impossible to do within the attribute declaration, but it’s easy to fix with an around helper:
around delegated_method (Any @args) {
$self->$orig($self, @args);
}(If you’re wrapping more than one method in this fashion, you should probably consider using a plain old Moose style around handler, which lets you wrap multiple methods with around @delegated_methods => sub {...}
So, at the end of all that, and after we’ve extracted our Type declarations into WeightVector::Types, we have:
use MooseX::Declare;
class WeightVector {
use WeightVector::Types qw(VectorOfWeights PercentageInt SlotOffset);
has vector => (
isa => 'VectorOfWeights',
is => 'ro',
required => 1,
traits => ['Array'],
handles {
weight_at => 'get',
},
);
method current_weight {
$self->weight_at(time());
}
around weight_at (SlotOffset $offset does coerce) {
$self->$orig($offset);
}
}And we’ve pushed all knowledge of DateTime off onto our type declarations and gained a boatload of handy bounds checking. We’ve also got a new tool for handling tricky delegation setups in the handles/around combo.
Notes
Motivation
I realise that this looks like a radical decomposition of the class with very little motivation, but it was driven by tests and by some other requirements that I’ve removed from the body of the post. In particular, the type coercions were driven by the need to build particular vectors for testing, a key method being:
method set_weight (PercentageInt $weight,
SlotOffset $from does coerce,
SlotOffset $to does coerce)
{
...
}Type coercion is wonderful
Generally, I’m not a fan of static typing. I’m from the “duck type all the way” school of programming,3 so most of my method declarations have no type declarations. But type declarations, especially ones that coerce, make so much sense on methods that make up the public protocol of a class. I only use type declarations on internal methods when I need a narrower coercion, or if I’m using MooseX::Multimethods, which I still haven’t used for anything but exploration.
Updates
Thanks to Chris Dolan for spotting that I’d got the SlotOffset coercion completely wrong. The real code’s doing the right thing, but that’s what comes of recreating code from memory.
1 This was actually motivated by trying to write tests to verify that the weights were correctly set.
2 I’m declaring these in a BEGIN block of the class itself mostly for explanatory purposes – there’s a good case for moving them out into a separate file and pulling it in with use.
3 Except during my periodic attempts to learn Haskell. I’ve learned Haskell at least three times now.
London.pm Presentation Video
Back in (crikey) February, I gave a talk at the London Perl Mongers’ technical meeting about Moose for Ruby Programmers and wrote it up here. Mike Whittaker was in the front row of the audience with his iPhone and, a couple of minutes in, started a voice recording and gave me a copy.
So… finally… I’ve taken the time I should have been using to write another article for The H and wrestled the slides and the audio into something like sync and uploaded the results to Vimeo for your viewing pleasure.
An introduction to MooseX::Declare from Piers Cawley on Vimeo.
