Delegation via Roles

DBIx::Class::DeploymentHandler is nearly ready for prime time, so I’m going to discuss a pattern mst described to me that I’ve found very helpful in developing this project.

🔗 Roles

If you don’t already know what roles are you probably don’t read very many perl blogs etc. chromatic has written a series of blog posts where he discusses the various merits of roles vs whatever your poison is. Maybe read that. This isn’t really about that. What this is about though is that roles aren’t always the answer.

🔗 Delegation

One of the assumptions of roles is that all of the methods in a role share their namespace. So if you compose Role1 and Role2 and they both implement a sleep method you will get an error at compile time saying that the method collides. This is a Good Thing and helps us not shoot ourselves in the foot. When it’s a problem is with private methods that the end user typically shouldn’t be calling, but happen to collide. As far as I know there is no way to have a role that partially composes with a class. I’m pretty sure that’s against the whole spirit of a role.

So instead of using a role to compose in the interface for whatever it is you are doing you can instead use delegation, where object A has-a different object B and uses the public interface of object B. That way private methods of B stay that way and don’t collide. The problem is that this can make code a lot more verbose. So instead of

$dh->deploy

one must do:

$dh->deploy_method->deploy

That make things a lot more verbose, it gives away the inner workings of $dh, and most importantly it makes overriding parts of $dh harder.

🔗 Roles with Delegation

So basically the pattern goes like this:

🔗 The Public Interface:

package HandlesFooing;

requires 'foo';
requires 'bar';

1;

🔗 The Delegate:

package Im::A::Delegate;
use Moose;
with 'HandlesFooing';

has foo => (
   is => 'ro',
   isa => 'Str',
   required => 1,
);

has bar => (
   is => 'ro',
   isa => 'Str',
   lazy_build => 1,
);

sub _build_bar { 'silly }

1;

🔗 The Role:

package WithDelegate;
use Moose::Role;
use Im::A::Delegate;

has foo => (
   is => 'ro',
   isa => 'Str',
   required => 1,
);

has bar => (
   is => 'ro',
   isa => 'Str',
   lazy_build => 1,
);

has delegate => (
   is => 'ro',
   isa => 'Im::A::Delegate',
   handles => 'HandlesFooing',
   lazy_build => 1,
);

sub _build_delegate {
   my $self = shift;
   my $args = {
      foo => $self->foo
   };

   $args->{bar} = $self->bar if $self->has_bar;
   Im::A::Delegate->new($args);
}

1;

🔗 Usage:

package GetStuffDone;
use Moose;
with 'WithDelegate';

1;

And to use that you’d do:

use GetStuffDone;
my $gsd = GetStuffDone->new(
   foo => 'frewfrew',
);

Of course that’s totally contrived, but it gets the general pattern across. If you want to see examples in action check out some of the roles from DBIx::Class::DeploymentHandler!

So basically you define your public interface, which isn’t a bad idea anyway, and the “handles” key for the delegate’s attribute takes the role that defines the public interface. This automatically delegates all the methods required by the role (and probably any defined by the role too.)

If you have private methods you want to reuse make another role and compose that into the delegate’s class, but don’t put it in the handles section.

I feel like this pattern, even though it yields a lot of boilerplate, helps to make very clean interfaces. Because of this pattern I’ve made well decomposed classes and testing them is dead easy. I imagine that to test roles normally you make stub classes using the roles. Here I just test the actual delegates alone and then I have an integration test that ensures the class that uses all the roles does the right thing.

Hopefully you’ll find this as useful as I have.

Posted Fri, Apr 2, 2010

If you're interested in being notified when new posts are published, you can subscribe here; you'll get an email once a week at the most.