Moose: A postmodern object system for Perl (part 4)

[ Perl tips index ]
[ Subscribe to Perl tips ]

This tip is part 4 in a series. See also part 1, part 2 and part 3.

Method modifiers: before, after and around

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"; };

Method resolution order (MRO)

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:

  1. PerlTrainer,

  2. PerlHacker,

  3. Programmer,

  4. 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:

  1. PerlTrainer,

  2. PerlHacker,

  3. Geek,

  4. Programmer.

C3 Method Resolution and Moose

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.

Passing things on

The mro pragma (but not MRO::Compat) provides three useful methods in a similar vein to the NEXT module.

next::method

Calls the next method of the same name in the C3 MRO. Throws an exception if there are no more methods of that name.

next::can

Returns a code reference to the next method in the C3 MRO if it exists, undef otherwise.

maybe::next::method

Combines 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.

Further reading

[ 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

Valid XHTML 1.0 Valid CSS