Tue, 30 Jun 2009
Transparent backwards compatibility using Moose
The current Net::Twitter is a complete rewrite of a prior version using Moose. One of my design goals was transparent backwards compatibility so that existing code based on Net::Twitter would continue to run, unchanged (provided the additional required modules were installed).
By using Moose roles I was able to factor out optional features and legacy behavior into separate classes. The core functionality became Net::Twitter::Core. Net::Twitter itself became an object factory applying appropriate roles to create a concrete classes including the appropriate mix of optional features then instances of those classes.
The Legacy role provides attributes and functionality that was not
carried forward into Net::Twitter::Core from Net::Twitter version 2.12. It
modifies the new core functionality with Moose method modifiers (in
particular, the around
modifier). It also applies the set of other,
optional roles required to make Net::Twitter backwards compatible.
The interesting and challenging work was getting the Net::Twitter factory
class correct. Initially, I used MooseX::Traits in Net::Twitter::Core
which provides the role application feature I needed with its
new_with_traits
method. But simply having Net::Twitter->new
call
Net::Twitter::Core->new_with_traits
wasn’t sufficient for Net::Twitter
derived classes. I ended up stealing a bit of the code and the concepts from
MooseX::Traits to get the behavior I needed.
Net::Twitter’s only public method is new
. It can be called with a traits
argument, or with a legacy
argument which is just a shortcut to save a bit
of typing. Traits and roles are the same thing. I use the terms
interchangeably here.
# The following are all equivalent, creating objects
# backwards compatible with Net::Twitter 2.12:
my $nt = Net::Twitter->new;
my $nt = Net::Twitter->new(legacy => 1);
my $nt = Net::Twitter->new(traits => ['Legacy']);
# New can also create non-legacy variations
my $nt = Net::Twitter->new(traits => ['API::REST', 'OAuth']);
Here is the complete new
method:
sub new {
my $class = shift;
croak '"new" is not an instance method' if ref $class;
my %args = @_ == 1 && ref $_[0] eq 'HASH' ? %{$_[0]} : @_;
my $traits = delete $args{traits};
if ( defined (my $legacy = delete $args{legacy}) ) {
croak "Options 'legacy' and 'traits' are mutually exclusive. Use only one."
if $traits;
$traits = [ $legacy ? 'Legacy' : 'API::REST' ];
}
$traits ||= [ qw/Legacy/ ];
$traits = [ $class->$resolve_traits(@$traits) ];
my $superclasses = [ 'Net::Twitter::Core' ];
my $meta = $create_anon_class->($superclasses, $traits, 1);
# create a Net::Twitter::Core object with roles applied
my $new = $meta->name->new(%args);
# rebless it to include a subclass, if necessary
if ( $class ne __PACKAGE__ ) {
unshift @$superclasses, $class;
my $final_meta = $create_anon_class->($superclasses, $traits, 0);
bless $new, $final_meta->name;
}
return $new;
}
The new
method expects either a single HASH reference or a list of key/value
pairs. It checks for and dereferences a HASH ref argument. Then it
normalizes the optional legacy
argument to the appropriate traits. If no
traits
or legacy
argument is provided, it uses the default Legacy
trait.
The first interesting bit of code is the call to $create_anon_class
, which
may be called twice.
The first call to $create_anon_class
creates a meta-class from
Net::Twitter::Core
with the appropriate roles applied and assigns it to the
variable $meta
. Anonymous classes aren’t /really/ anonymous; they have
auto-generated names. So, after creating the meta class, we create an
instance of the class it represents and assign the instance to the variable
$new
.
If Net::Twitter->new
was called directly, we’re done. We can simply return
the new instance. However, if new
was called from some Net::Twitter derived
class we need to create another anonymous class with the derived class
prepended to the list of superclasses. It gets assigned to the variable
$final_meta
.
We couldn’t create the first meta class with the derived class specified in
superclasses
, because calling new
on the class it represents is likely to
be infinitely recursive!
Consider this Net::Twitter derived class:
package My::Net::Twitter::DerivedClass;
use base 'Net::Twitter';
sub some_new_method { ... }
Net::Twitter’s new
method will be called with the $class
argument
My::Net::Twitter::DerivedClass
. Calling new
on the resulting anonymous
class, with My::Net::Twitter::DerivedClass
in superclasses
will result in
an infinite recursion since Net::Twitter’s new
will be called, again.
So, after creating an instance of the anonymous class without
My::Net::Twitter::DerivedClass
in superclasses
, we create another
anonymous class and assign it to the variable $final_meta
. Then we rebless
our existing instance into the new anonymous class and return it. Reblessed
into the new class, it can find and use the derived class methods.
$create_anon_class
has some interesting features.
my $create_anon_class = sub {
my ($superclasses, $traits, $immutable) = @_;
my $meta;
$meta = Net::Twitter::Core->meta->create_anon_class(
superclasses => $superclasses,
roles => $traits,
methods => { meta => sub { $meta }, isa => $isa },
cache => 1,
);
$meta->make_immutable(inline_constructor => $immutable);
return $meta;
};
With the cache
argument set to 1
, create_anon_class
caches the classes
it creates and returns an existing instance of the mata-class when the same
arguments are passed again. So, although create_anon_class
may be called an
arbitrary number of times in the life of an application, a new, anonymous
class is only created when different roles or superclasses are specified.
A final point of interest is the method isa
added to to the anonymous
classes.
my $isa = sub {
my $self = shift;
my $isa = shift;
return $isa eq __PACKAGE__ || $self->SUPER::isa($isa)
};
Net::Twitter is an object factory. It does not create instances of itself.
It creates instances of anonymous classes based on Net::Twitter::Core with
roles applied. Classes derived from Net::Twitter may be surprised (and die!)
if ->isa('Net::Twitter')
fails on their instances. So, objects created by
Net::Twitter claim to be Net::Twitter instances when queried with isa
.
Net::Twitter contains a bit more code than I’ve shown here. The additional
code, stolen from MooseX::Traits, simply expands role names, passed in the
traits
argument into the Net::Twitter::Role namespace.
This may not be the best possible implementation, but it seems to work well
for all the conditions I have encountered tested. Your feed back is welcome.
You can send me email or find me online. I’m semifor
on IRC and can usually
be found in the #moose and #net-twitter channels on irc.perl.org. I’m also
semifor
on Twitter.
About this weblog
This site is the personal weblog of Marc Mims. You can contact Marc
by sending e-mail to:
[email protected].
Marc writes here about cycling, programming, Linux, and other items of personal interest.
This site is syndicated with RSS.
Credits
CSS stolen from Tom Coates who didn't even complain.