[ Perl tips index ]
[ Subscribe to Perl tips ]
This tip is part 4 in a series. See also part 1, part 2 and part 3.
One of Moose's features is the easy ability to extend existing
methods for our object using the new keywords before, after and
around. These avoid cumbersome walking of the inheritance tree, as well
as allowing roles to modify existing methods.
Using before allows us to inject code before the role method is called,
after allows us to inject code after the role method is called and
around allows us to do both. This is useful if we want to emit
debugging information, log something to a file, pre-open a file, change
@_ or any of many things. In the following case we can demonstrate how
these might help us provide some extra debugging information on a Logger
class:
# Logger.pm
package Logger;
use Moose::Role;
# Print given message to given file handle
sub log {
my ($self, $fh, $message) = @_;
print {$fh} $message;
}
no Moose::Role;
1;
##########
# Logger/Debug.pm
package Logger::Debug;
use Moose;
with 'Logger';
# Do this before we call 'log'
before 'log' => sub {
print STDERR "About to log\n";
};
# Do this after we call 'log'
after 'log' => sub {
print STDERR "Finished logging\n";
};
# Wrap this around the call to log, but after the 'before' above,
# and before the 'after' above.
around 'log' => sub {
my $next = shift;
my $self = shift;
print STDERR " Around the call to log\n";
# Pass in a handle to STDERR instead
# Pass in our own message:
$self->$next( \*STDERR, " Inside log\n" );
print STDERR " Still around the call to log\n";
};
no Moose;
1;
Now if we use this in our program:
#!/usr/bin/perl -w
use strict;
use warnings;
use autodie;
use Logger::Debug;
open (my $fh, ">", "/tmp/logfile.txt");
Logger::Debug->new->log($fh, "test\n");
we will get:
About to log
Around the call to log
Inside log
Still around the call to log
Finished logging
As you can see, first any before modifiers are called, then any
around modifiers (which should themselves call the desired method but
may not) and then finally the after modifiers.
In this example our around call supplants the original arguments passed
to log and instead creates its own. This is useful for debugging and
testing, but obviously not useful as a general rule.
before, after and around are Moose methods which take two
arguments; the name of the method they are modifying and a subroutine
reference containing the modification. As such it is important to remember
to complete each method call with a semi-colon.
# Wrong, missing final semi-colon (syntax error)
before 'log' => sub { print "before"; }
# Correct
before 'log' => sub { print "before"; };
In standard Perl 5 OO, methods are resolved using the rule that the
left-most-ancestor wins. Thus the first method found in a depth-first
search through your object's hierarchy will be the only method called
unless it is using the NEXT module to pass the request back to the
dispatcher.
One problem with this rule is that in some circumstances a class' method may be called before its subclass has had an attempt. For example if we have the following diamond inheritance:
Programmer
/ \
PerlHacker Geek
\ /
PerlTrainer
In standard OO Perl, we will walk through our classes looking for an appropriate method in this order:
PerlTrainer,
PerlHacker,
Programmer,
Geek.
If both Geek and Programmer have a method of the name we're looking
for (while PerlTrainer and PerlHacker do not). Then it will be the
method provided by the Programmer class that gets called - even if the
method in Geek is more appropriate. In fact, by default the method in
Geek will never be called. Only if a re-dispatch class such as NEXT
is used will the method in Geek be called, and then only after that
in Programmer.
In some cases this might be acceptable, but in many cases this could be the cause of numerous annoying problems. We may instead want to ensure that we never call a superclass' method until all of its subclasses have had a chance first. If we could impose such a rule, we would then walk through our classes checking for methods in the following order:
PerlTrainer,
PerlHacker,
Geek,
Programmer.
The mro pragma (or MRO::Compat for Perl 5.6 and 5.8) allows you to
choose which method resolution order you would prefer. You can choose
between Perl's default (depth-first-search: dfs) and C3 (c3).
C3 always preserves local precedence ordering. Thus in the example above, C3 ordering gives us the order we want, where all subclass methods are called before any superclass.
Moose uses C3 method ordering by default.
For more information on C3 and MRO choices, read perldoc mro for
Perl 5.10 and above or perldoc MRO::Compat for Perls 5.6 and 5.8.
The mro pragma (but not MRO::Compat) provides three useful methods
in a similar vein to the NEXT module.
next::methodCalls the next method of the same name in the C3 MRO. Throws an exception if there are no more methods of that name.
next::canReturns a code reference to the next method in the C3 MRO if it exists,
undef otherwise.
maybe::next::methodCombines next::method and next::can to ensure that the method is only
called if it exists. Similar to writing:
$self->next::method(@_) if $self->next::can;
We can use use these methods from within our Moose classes too (so long as we have Perl 5.10 or above).
# PerlTrainer.pm
package Programmer;
use Moose;
sub review {
my $self = shift;
print "Programmer review\n";
$self->maybe::next::method(@_);
}
no Moose;
package PerlHacker;
use Moose;
extends 'Programmer';
sub review {
my $self = shift;
print "PerlHacker review\n";
$self->maybe::next::method(@_);
}
no Moose;
package Geek;
use Moose;
extends 'Programmer';
sub review {
my $self = shift;
print "Geek review\n";
$self->maybe::next::method(@_);
}
no Moose;
package PerlTrainer;
use Moose;
extends 'PerlHacker', 'Geek';
sub review {
my $self = shift;
print "PerlTrainer review\n";
$self->maybe::next::method(@_);
}
no Moose;
1;
and in our calling code:
use PerlTrainer;
PerlTrainer->new()->review();
This gives us:
PerlTrainer review
PerlHacker review
Geek review
Programmer review
as expected. If we were certain that our inheritance tree was finalised,
we may have used next::method instead of maybe::next::method.
For Perl 5.10 and later, the use of mro is preferred over NEXT.
[ Perl tips index ]
[ Subscribe to Perl tips ]
This Perl tip and associated text is copyright Perl Training Australia. You may freely distribute this text so long as it is distributed in full with this Copyright noticed attached.
If you have any questions please don't hesitate to contact us:
| Email: | contact@perltraining.com.au |
| Phone: | 03 9354 6001 (Australia) |
| International: | +61 3 9354 6001 |
Copyright Perl Training Australia. Contact us at contact@perltraining.com.au