Subclassing Tricky Non-Moose Classes: Constructor Problems

We have a non-Moose class but want to make a Moose subclass of it. In the first post, "Subclassing Tricky Non-Moose Classes: Don't Do It", we looked at a way to extend non-Moose classes without actually subclassing them. It is pretty straight-forward, and typically will cause less headaches.

Sometimes that method might not meet your needs, and you might really want to make a Moose subclass of a non-Moose class. This tutorial will get you started with a couple modules that will help you do just that: MooseX::NonMoose and MooseX::NonMoose::InsideOut.

The nitty-gritty

If you're looking to get your hands dirty and find out how this really works, your first stop should be Recipe 11 in the Moose Cookbook. It is optional reading for our purposes, though. It does a great job explaining how to subclass a common, blessed hash, non-Moose class "by hand," and points us to MooseX::NonMoose which handles those details for us.

We will take advantage of this handy module, and push forward to examine some trickier situations that can arise when subclassing non-Moose classes.

1) The simplest case

(Follow along with the example 1 gist)

In most circumstances using MooseX::NonMoose couldn't be easier. Let's imagine we have this: Animal1.pm. It is a trivial example of an animal class, which takes a hash reference of attributes in the constructor like this
Animal1->new({ name => 'Lassie' })
and provides an accessor to name that allows us to set and get that attribute.

In order to subclass this, all we need to do is add
use MooseX::NonMoose; extends 'Animal1';
Let's do this to create a subclass called Camel with an additional attribute called humps to specify the number of humps on its back. Our new class looks like this:

Camel example 1:
package Camel;
use Moose;
use MooseX::NonMoose;
extends 'Animal1';

has 'humps' => ( is  => 'ro', isa => 'Num' );

no Moose;
__PACKAGE__->meta->make_immutable;

We can now create a Camel object the same way we did with Animal1, but with the additional humps attribute. Here is our example script:

Example script 1:
#!/usr/bin/perl
use Modern::Perl;
use Camel;

my $c = Camel->new({ name => "Samuel Camel", humps => 2 });
say $c->name, " has ", $c->humps, " humps";

This produces

Samuel Camel has 2 humps

Most of the time subclassing a non-Moose class is as simple as this. MooseX::NonMoose does all the work for us.

2) Incompatible constructors

Sometimes, however, it isn't so easy.

What if our legacy class looked like this instead: Animal2.pm. It's largely the same as Animal1, except has a different constructor. Instead of providing it with a hash reference of attributes, we simply pass it the animal's name:
Animal2->new("Lassie");

We'll use the exact same Camel subclass as in Camel example 1, just change "extends 'Animal1'" to "extends 'Animal2'". Now let's try the Example script from above again.

Oh dear. This is not what we wanted...

HASH(0x100803ee8) has 2 humps

Moose expects hash or hashref arguments passed to the constructor, but Animal2 wants a string. Moose saw the first (and only) argument was a hashref, { name => "Samuel Camel", humps => 2 }, as it expected, and stored the name attribute. Animal2, on the other hand, does not accept hash references and simply tried to treat it as the animal's name (producing that "HASH(0x100...)").

Let's look at two ways we can modify Camel to resolve this problem:

  1. accept a hash of attribute values in the constructor, breaking compatibility with Animal2
  2. accept a single argument in the constructor like Animal2

2.1) Resolving incompatible constructors - Using hashes

(Follow along with the example 2.1 gist)

Even though Animal2 doesn't support hashes passed into its constructor, we can modify Camel to allow it, enabling our example script from before to do what we wanted.

We looked at BUILDARGS in our last tutorial, which allows us to modify the arguments sent to the Moose constructor. In this example, however, we need to modify the arguments sent to the Animal2 constructor.

MooseX::NonMoose provides a similar method, FOREIGNBUILDARGS, that allows us to determine what gets sent to the non-Moose parent constructor. It works like this:

# FOREIGNBUILDARGS is not declared anywhere in our parent classes,
# so we can just override it, rather than wrap it in 'around'
sub FOREIGNBUILDARGS {
    my $class = shift;
    # The remaining arguments were sent to the constructor.
    # e.g. if you called YourClass->new({ foo => 'bar' }), then 
    # @args contains one element, that hashref.
    my @args = @_;

    # ... Make any modifications to @args here.

    # Whatever we return will be sent to new() in the non-Moose parent
    return @args;
}

Now take those principles and apply them to our problem in order to provide the Animal2 constructor with a single string argument.

Camel example 2.1:
package Camel;
use Moose;
use MooseX::NonMoose;
extends 'Animal2';

has 'humps' => ( is  => 'ro', isa => 'Num' );

sub FOREIGNBUILDARGS {
    my $class = shift;
    # We expect a hashref with 'name' and 'humps'.
    my $args = shift;

    # Give Animal2 what it wants: just the name, and nothing else.
    return $args->{name};
}

no Moose;
__PACKAGE__->meta->make_immutable;

2.2) Resolving incompatible constructors - Maintaining compatibility with parent class

(Follow along with the example 2.2 gist)

NOTE: I do not recommend using this solution unless you must. Passing hashes to the constructor as we did in solution in 2.1 results in more readable and maintainable code. That being said...

If we want our Camel class to be a drop-in replacement for Animal2, we need the ability to pass our constructor just the name, rather than a hash. But what about the number of humps? We could change the humps attribute to be read-write (is => 'rw' instead of 'ro') and create our object like this: my $c = Camel->new("Samuel Camel"); $c->humps(2);

But this can be painful, especially when we're dealing with more complicated non-trivial classes. We probably prefer to leave humps readonly and instead pass the humps in the constructor like this: Camel->new("Samuel Camel", 2);

Let's start back with the bare-bones Camel example 1, which does not mess with BUILDARGS or FOREIGNBUILDARGS. If we try to use this with a constructor like this, Camel->new("Samuel Camel"); we get the following error:

Single parameters to new() must be a HASH ref at...

If you try to call Camel->new("Samuel Camel", 2) it won't throw an error, but it also will not store "2" for the number of humps. The Moose parent will interpret that as a hash "Samuel Camel" => 2 with its "Samuel Camel" name being the key with the number of humps as the value.

In order to do this, Camel->new("Samuel Camel", 2) we need to intercept those arguments with BUILDARGS and give { humps => 2 } to the Moose parent, and use FOREIGNBUILDARGS to pass only the name "Samuel Camel" to the non-Moose parent.

Camel example 2.2;
package Camel;
use Moose;
use MooseX::NonMoose;
extends 'Animal2';

has 'humps' => ( is  => 'ro', isa => 'Num' );

# Make sure Moose::Object parent gets a hashref with 'humps' in it
around BUILDARGS => sub {
    my $orig  = shift;
    my $class = shift;
    # We expect this to be called with: Camel->new($name, $optional_humps)
    # so @_ contains a name, and possibly a number of humps.
    my $camel_name  = shift;
    my $camel_humps = shift;

    my $moose_args = {};
    $moose_args->{humps} = $camel_humps if defined $camel_humps;

    # Give Moose constructor what it wants
    return $class->$orig($moose_args);
};

# Make sure Animal2 parent only gets a name
sub FOREIGNBUILDARGS {
    my $class = shift;
    my $name = shift;
    my $humps = shift; # We are going to ignore this.

    # Whatever we return will be sent to Animal2->new()
    return $name;
}

no Moose;
__PACKAGE__->meta->make_immutable;
And now we have a drop-in replacement for Animal2 that can accept an optional second parameter in its constructor. Here's how we can use it:
Example script 2.2:
#!/usr/bin/perl
use Modern::Perl;
use Camel;

my $c = Camel->new("Samuel Camel", 2);
say $c->name, " has ", $c->humps, " humps";

3) Non-hash objects

(Follow along with the example 3 gist)

For our final example, consider Animal3.pm which is used exactly like Animal2, with a single string argument to its constructor: Animal3->new("Lassie");. Take either of the Camel class examples we created for Animal2 (either the one whose constructor takes hash-based arguments, or ordered arguments). Be sure to change its extends to the new Animal3 class.

If you try to use your new Camel class, you will get an error similar to this:

Not a HASH reference at generated method (unknown origin)...

Most Perl classes use a blessed hash behind the scenes, and that is what MooseX::NonMoose expects.

Even though Animal3 is used exactly like Animal2, this final example is implemented differenly inside. It uses a blessed array instead of a hash. If this doesn't mean much to you yet, don't worry about it! We have a simple solution:

In your Camel class, change the line that says use MooseX::NonMoose; to use MooseX::NonMoose::InsideOut.

When you encounter the "Not a HASH..." error while using MooseX::NonMoose, simply use MooseX::NonMoose:InsideOut instead. That's it! Everything else we've learned about BUILDARGS and FOREIGNBUILDARGS still applies.

Extra credit: Going the extra mile

In Camel example 1 and Camel example 2.1, we didn't utilize BUILDARGS for anything. This means our Moose::Object constructor was getting exactly the arguments we were passing into Camel. This includes the name attribute, which it has no knowledge of since it only exists in the non-Moose class.

To be a good citizen, we should probably strip out this name argument to keep it out of sight from our Moose parent.

Your mission, should you choose to accept it, is to add an around BUILDARGS => sub { ... }; to clean up the arguments so they only include those newly-added, Moosified attributes (i.e. just humps).

5 Comments

I know a lot of OO discussions use the Person-Employee example, but only because the languages they chose are so feature poor. Moose can do much better, can't it, and shouldn't it?

Inheritance should really only involve things that make up the inseparable identity of the thing. A person isn't really an employee, it's just a role—something the person does. You can separate the employee bits from the person, so in the design, you should be able to separate the employee part too. Think what happens to your object when the person is no longer an employee. Now there's no longer a person either because the employee bits are tightly coupled.

I tend to think in this example, people really want it the other way around. They want an employee object that references a person. The employee object merely tracks the things that matter to the employment relationship without extending anything. Moose has the stuff to do this right, and I hope the people writing about Moose start using the right tools for the right jobs.

Dave Rolsky and I talked quite a bit about this for his OO doc that we're thinking about adding to the perldocs. I like Randal's Animal tutorial in perlboot. You may not like that particular example, but Randal worked hard to create an example where the traits of the base classes where inseparable from the identity of the thing he modeled in the concrete classes.

I often find the the best way to extend a non-moose based object is by delegation:

If the non-moose class had sensible defaults you'd normally just need a builder method but as you can see you can initialize it in BUILD or BUILDARGS (as shown in gist).

Furthermore you can restrict just how much of the non-moose API you expose in your class.

oops! here is the example I meant to give above:

#!/usr/bin/env perl                                                                                           
use strict;
use warnings FATAL => 'all';
use Test::Most;

{
package Animal;
sub new { bless [ $_[1] ], $_[0] }
sub name { shift->[0] }
}

subtest "array-based Animal" => sub {
my $animal = new_ok "Animal", ["Lassie"];
can_ok $animal, "name";
is $animal->name, "Lassie", "name is Lassie";
};

{
package Camel;
use Moose;
has humps => ( is => "ro", isa => "Int" );
has animal => ( isa => "Animal", handles => ["name"] );
around BUILDARGS => sub {
my ($orig, $self, @args) = @_;
if (@args == 2) {
return { animal => Animal->new($args[0]), humps => $args[1] };
}
$self->$orig(@args);
}
}

subtest "moose-based Camel" => sub {
my $camel = new_ok "Camel", [ "Steve", 2 ];
can_ok $camel, "humps", "name";
is $camel->humps, 2, "has 2 humps";
is $camel->name, "Steve", "name is Steve";
};

done_testing;

Ps: Is it possible to provide code samples as github gists here? Otherwise how exactly does one get their code samples to show up all nice and pretty :)

Leave a comment

About Mark A. Stratman

user-pic Perl developer, forever trapped in the DarkPAN