Perl Moose and BUILDARGS
Moose::Object supports a method called BUILDARGS which is used to pre process incoming initialization arguments. By default you get one that allows you to initialize an object with either HASH or HASHREF arguments. However, you can wrap that method and do additional custom pre-processing. For example:
use Test::Most;
{
package Foo;
use Moose;
has foo => (is=>'ro');
around 'BUILDARGS', sub {
my ($orig, $class, %args) = @_;
if(my $bar = delete $args{bar}) {
$args{foo} ||= $bar;
}
$class->$orig(%args);
};
}
ok my $foo_thinks_is_bar = Foo->new(bar=>100);
is $foo_thinks_is_bar->foo, '100';
done_testing;
In this case we allow the Foo class to accept an initialization argument of 'bar' and map that to 'foo'. Ok this is a trivial example but it illustrated the point. It does the job.
However I have found ->BUILDARGS can be a code smell or lead to complex, hard to understand code. For example, when you have lots of attributes that need mapping or changing and lots of conditional logic in the BUILDARGS modifier, that can be hard to understand and hard to test. For people that don't know Moose it can be confusing. It also takes a lot of control away from the caller, and hides it in potentially twisty logic. Lastly, I think it tends to be a feature that gets overused rather than to properly model your logic.
For the most part I think that BUILDARGS should be limited to helping your Moose classes be compatible with non Moose code that might be expecting an initialization style that is different from the standard Moose ->new(%args) style. For example, you might have a class called Kettle that uses a Color object and you decide you want to rewrite Color in Moose. However Kettle initialized Colors with ->new($color_name). You can this:
use Test::Most;
{
package Color;
use Moose;
has 'name' => (is=>'ro', required=>1);
around 'BUILDARGS', sub {
my ($orig, $class, $name) = @_;
return $class->$orig(name=>$name);
};
package Kettle;
sub new {
my ($class, @args) = @_;
return bless {
color => Color->new('black'),
@args };
}
sub color_name { shift->{color}->name }
}
ok my $kettle = Kettle->new;
is $kettle->color_name, 'black';
done_testing;
I think this is a reasonable use case for BUILDARGS since its basically making Moose play nice with your existing non Moose stuff, and doesn't force anyone to understand what Moose is doing here nor are you getting back something other than what you expect.
So if BUILDARGS isn't always the right choices, what is? Well, there's actually a few programming design patterns that are generic across most OO programming languages and don't require a lot back story to understand. First, you can have a factory method on your class that does what you want. For example:
use Test::Most;
{
package Foo;
use Moose;
has foo => (is=>'ro');
sub new_with_bar {
my ($class, %args) = @_;
return $class->new(foo=>$args{bar});
}
}
ok my $foo_thinks_is_bar2 = Foo->new_with_bar(bar=>100);
is $foo_thinks_is_bar2->foo, '100';
done_testing;
Not only is this less lines of code then the BUILDARGS approach, but there's already some decent built in argument checking (at it would be trivial to add a bit more) and it lets the caller get something expected, rather than unexpected. And the approach scales pretty well:
use Test::Most;
{
package Foo3;
use Moose;
has foo => (is=>'ro');
sub new_with_bar {
my ($class, %args) = @_;
return $class->new(foo=>$args{bar});
}
sub new_with_bat {
my ($class, %args) = @_;
return $class->new(foo=>$args{bat});
}
}
ok my $foo_thinks_is_bar3 = Foo3->new_with_bat(bat=>100);
is $foo_thinks_is_bar3->foo, '100';
done_testing;
But what if you want a bit of magic, and not require your caller to choose the correct builder method? You can if you want create a builder dispatcher, which at least makes the logic reasonable separate:
{
package Foo4;
use Moose;
has foo => (is=>'ro');
sub construct {
my ($class, %args) = @_;
if($args{bar}) { return $class->new(foo=>$args{bar}) }
if($args{bat}) { return $class->new(foo=>$args{bat}) }
}
sub new_with_bar {
my ($class, %args) = @_;
return $class->new(foo=>$args{bar});
}
sub new_with_bat {
my ($class, %args) = @_;
return $class->new(foo=>$args{bat});
}
}
is((Foo4->construct(bar=>200)->foo), 200);
is((Foo4->construct(bat=>300)->foo), 300);
done_testing;
You can probably think of more elegant ways to write that dispatcher (I'd probably make it a separate class rather than inlined with the construct method since that would be more reusable and testingable) but at least this version has the big upside of being clear.
So this option I think is better for most cases when you are thinking of reaching for BUILDARGS. There is another approach that I tend to now favor, however. One issue with 'Foo4' is that you are mixing a lot of instantiation logic with actual 'Foo'ness which I think can become distracting to the person reading the class. Ideally I think a class should do one thing.
We could create a separate class to handle any complex instantiation work. There's a classic design pattern called the 'builder pattern (read up on it here "http://en.wikipedia.org/wiki/Builder_pattern") and if you take a look at that article here's my Moosey interpretation:
use Test::More;
{
package Pizza;
use Moose;
has [qw/dough sauce topping/] => (is=>'rw');
package PizzaBuilder;
use Moose::Role;
requires qw/build_dough build_sauce build_topping/;
has pizza => (is=>'ro', isa=>'Pizza',
builder=>'_build_pizza', required=>1);
sub _build_pizza { Pizza->new }
package HawaiianPizzaBuilder;
use Moose;
with 'PizzaBuilder';
sub build_dough { shift->pizza->dough('cross') }
sub build_sauce { shift->pizza->sauce('mild') }
sub build_topping { shift->pizza->topping('ham+pineapple') }
package SpicyPizzaBuilder;
use Moose;
with 'PizzaBuilder';
sub build_dough { shift->pizza->dough('pan baked') }
sub build_sauce { shift->pizza->sauce('hot') }
sub build_topping { shift->pizza->topping('pepperoni+salami') }
package Waiter;
use Moose;
has 'pizza_builder' => (is=>'rw', does=>'PizzaBuilder');
sub pizza { shift->pizza_builder->pizza }
sub construct_pizza {
my ($self) = @_;
$self->pizza_builder->$_ for (qw/
build_dough build_sauce build_topping/);
}
}
ok my $waiter = Waiter->new;
ok my $hawaiian_pizza_builder = HawaiianPizzaBuilder->new;
ok my $spicy_pizza_builder = SpicyPizzaBuilder->new;
for($waiter) {
$_->pizza_builder($hawaiian_pizza_builder);
$_->construct_pizza;
}
is $waiter->pizza->sauce, 'mild';
for($waiter) {
$_->pizza_builder($spicy_pizza_builder);
$_->construct_pizza;
}
is $waiter->pizza->sauce, 'hot';
done_testing;
Ok, that's a lot of classes, and I'd probably alter it a bit since after all Perl is a dynamic language and some of the hoops the classic builder pattern requires are based on the fact that Java and others are static, but I think you get the idea. Your more Perly interpretations are very welcomed!
So overall there's a lot of options you have when there's complex initialization requirements, most of which are patterns that are cross language, which has a nice upside that you have code more likely to be understood by people new or Moose and you can draw upon the large thought ecosystem regarding these types of patterns.
Here's a gist of all the example code in a single test case you should be able to run directly.
This has been a great help to me, Moose makes Perl Object oriented Programing much easier and consistent which is turn makes your job less tedious.
Posted by: liza | 10/29/2012 at 03:25 AM