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.
You did a pretty good job explaining how to do this. Can you also write a bit about why one would do this?
Thanks Mithaldu. I can definitely add some "why" to it. If you have some particular reasons you'd like to see, let me know.
Otherwise for now I'll just add the main, practical reason (as opposed to for the sake of good design) - that it's often a PITA to subclass.
You may be discussing this in your next article, but have you found MooseX::NonMoose to be lacking?
No - in fact this article as originally going to be about MooseX::NonMoose (and InsideOut)... until mst and doy convinced me that most of the people who want to subclass them SHOULDn't.
So ... being a tutorial... i figured it should *first* focus on the "best" answer.
The followup will be what I originally intended to write - about MooseX::NonMoose, and how to use them and overcome common problems.
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?