DEV Community

Cover image for Taming the Moose: Classing up Perl attributes
Mark Gardner
Mark Gardner

Posted on • Originally published at phoenixtrap.com on

Taming the Moose: Classing up Perl attributes

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.
Enter fullscreen mode Exit fullscreen mode

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',
);
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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} );
}
Enter fullscreen mode Exit fullscreen mode

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,
    );
}
Enter fullscreen mode Exit fullscreen mode

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

use MooseX::ClassAttribute;

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

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 Inkster’s 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.

Top comments (0)