At my work, we extensively use the Moose object system to take care of what would ordinarily be very tedious boilerplate object-​oriented Perl code. In one part of the codebase, we have a family of classes that, among other things, map Perl methods to the names of various calls in a third-​party API within our larger organization. Those private Perl methods are in turn called from public methods provided by roles consumed by these classes so that other areas aren’t concerned with said API’s details.

Without going into too many specifics, I had a bunch of classes all with sections that looked like this:

sub _create_method    { return 'api_add'     }
sub _retrieve_method  { return 'api_info'    }
sub _search_method    { return 'api_list'    }
sub _update_method    { return 'api_update'  }
sub _cancel_method    { return 'api_remove'  }
sub _suspend_method   { return 'api_disable' }
sub _unsuspend_method { return 'api_restore' }

... # etc.

The values returned by these very simple methods might differ from class to class depending on the API call needed, and different classes might have a different mix of these methods depending on what roles they consume.

These methods had built up over time as developers had expanded the classes’ functionality, and this week it was my turn. I decided to apply the DRY (don’t repeat yourself) principle and create them from a simple hash table like so:

my %METHOD_MAP = (
  _create_method    => 'api_add',
  _retrieve_method  => 'api_info',
  _search_method    => 'api_list',
  _update_method    => 'api_update',
  _cancel_method    => 'api_remove',
  _suspend_method   => 'api_disable',
  _unsuspend_method => 'api_restore',
);

At first, I thought to myself, These look like private read-​only attributes!” So I wrote:

use Moose;

...

has $_ => (
  is       => 'ro',
  init_arg => undef,
  default  => $METHOD_MAP{$_},
) for keys %METHOD_MAP;

Of course, I’d have to move the classes’ with statements after these definitions so the roles they consume could see” these runtime-​defined attributes. But some of the methods used to read these are class methods (e.g., called as ClassName->foo() rather than $object->foo()), and Moose attributes are only set after the construction of a class instance.

Then I thought, Hey, Moose has a MOP (meta-​object protocol)! I’ll use that to generate these methods at runtime!”

my $meta = __PACKAGE__->meta;

while (my ($method, $api_call) = each %METHOD_MAP) {
    $meta->add_method( $method => sub {$api_call} );
}

The add_method documentation strongly encourage[s]” you to pass a metamethod object rather than a code reference, though, so that would look like:

use Moose::Meta::Method;

my $meta = __PACKAGE__->meta;

while (my ($method, $api_call) = each %METHOD_MAP) {
    $meta->add_method( $method = Moose::Meta::Method->wrap(
      sub {$api_call}, __PACKAGE__, $meta,
    );
}

This was getting ugly. There had to be a better way, and fortunately there was in the form of Dave Rolskys MooseX::ClassAttribute module. It simplifies the above to:

use MooseX::ClassAttribute;

class_has $_ => (
  is      => 'ro',
  default => $METHOD_MAP{$_},
) for keys %METHOD_MAP;

Note there’s no need for init_arg => undef to prevent setting the attribute in the constructor. Although they’re still Moose attributes, they act like class methods so long as the class consumes the roles that require them after the attribute definitions.

Lastly, if we were using Moo as a lightweight alternative to Moose, I could have instead selected Toby Inksters MooX::ClassAttribute. Although it has some caveats, it’s pretty much the only alternative to our initial class method definitions as Moo lacks a meta-​object protocol.

The lesson as always is to check CPAN (or the appropriate mix of your language’s software repository, forums like Stack Overflow, etc.) for anything that could conceivably have application outside of your particular circumstances. Twenty-​five years into my career and I’m still leaping into code without first considering that someone smarter than me has already done the work.

One thought on “Taming the Moose: Classing up Perl attributes

Comments are closed.