Subclassing Tricky Non-Moose Classes: Don't Do It

We have a non-Moose class but want to make a Moose subclass of it. First, step back and consider if we really need a subclass.

Don't subclass

There are probably some good arguments against subclassing non-Moose classes with a Moose class that center on principles of good design, and applying the most suitable design patterns. From a practical standpoint, though, there's a very simple reason to avoid it: using Moose to subclass a non-Moose class is fraught with "gotchas." We'll see some of these problems in an upcoming post, but for now let's look at an alternative to subclassing.

Perhaps a better option is to simply create your a new class and use delegation to call methods on an accessor containing your non-Moose object.

Let's say we initially wanted to subclass Date::Handler, but we decided to try delegation instead. After reading through Moose::Manual::Delegation we come up with the following:

package MyDate;
use Moose;
use namespace::autoclean;
use Date::Handler;

has 'date_handler' => (
    is => 'ro',
    isa => 'Date::Handler',
    handles => qr/.*/,
);

The "handles" option allows us to specify regular expressions, so rather than writing each of the countless methods Date::Handler provides, we simply specify that the date_handler attribute handles => qr/.*/ . Now our MyDate object will catch any method call and delegate it to the Date::Handler object in date_handler. In other words, instead of needing to write $my_date->date_handler->date_handler_method() we can write $my_date->date_handler_method().

There is still a major problem though. We initially wanted a subclass which, presumably, would act as a drop-in replacement. Our new class fails at this because we need to set the date_handler attribute:

MyDate->new(date_handler => Date::Handler->new(\%date_handler_args));

This is fairly inconvenient, and moreover, does not behave like a subclass would.

A drop-in replacement

We need to tell our MyDate->new(...) method to take the same arguments as Date::Handler does. Fortunately Moose::Object provides a method called BUILDARGS that lets us modify the arguments sent to the constructor. Overriding this can be problematic if we're not careful, so we'll use the around method modifier to modify the arguments being sent to BUILDARGS.

around BUILDARGS => sub {
    my $orig = shift;
    my $class = shift;
    return $class->$orig(date_handler => Date::Handler->new(@_));
};

When calling $orig() (i.e. BUILDARGS), we pass in the date_handler value automatically so the calling code no longer needs to worry about it. Notice that we also DO NOT pass the value from @_ to the constructor of our new MyDate class, but instead give it to the Date::Handler constructor.

We now have the ability to call MyDate->new(\%date_handler_args), and otherwise treat this new class and its objects just as we would a Date::Handler.

Let's go ahead and add a leading underscore to the _date_handler attribute. This isn't necessary, but it indicates that it is private and that the end-user should not worry about it.

Decorating a non-Moose class

Putting it all together, we get the following (gist).

package MyDate;
use Moose;
use namespace::autoclean;
use Date::Handler;

has '_date_handler' => (
    is => 'ro',
    isa => 'Date::Handler',
    handles => qr/.*/,
);
around BUILDARGS => sub {
    my $orig = shift;
    my $class = shift;
    return $class->$orig(_date_handler => Date::Handler->new(@_));
};

no Moose;
__PACKAGE__->meta->make_immutable;

But I REALLY want a subclass

If you really want to make a Moose subclass of a non-Moose class, check out the following post where we cover some examples of subclassing.

Credits

Thanks to doy, perigrin, and mst (and his mallet). I started writing some tutorials on subclassing tricky non-Moose classes, but they made the case that even if that's what you want to do, it's probably not what you should do.

5 Comments

You did a pretty good job explaining how to do this. Can you also write a bit about why one would do this?

You may be discussing this in your next article, but have you found MooseX::NonMoose to be lacking?

Thanks, the explanation bit you added near the top really makes it clearer. I assume the gotchas will be things like being unable to properly apply moose stuff like method modifiers and such?

Leave a comment

About Mark A. Stratman

user-pic Perl developer, forever trapped in the DarkPAN