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

[ Perl tips index ]
[ Subscribe to Perl tips ]

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

Inheritance and Roles in Moose

In regular Perl, inheritance is handled by either manipulating the @ISA array directly; or by using the base pragma. Moose abstracts those issues away with the extends keyword.

For example, consider the following inheritance tree:

        Trainer
                PerlTrainer

We can create our Trainer class as follows:

        package Trainer;
        use Moose;

        has 'name'      => ( is => 'rw', isa => 'Str', required => 1 );
        has 'subject'   => ( is => 'rw', isa => 'Str', required => 1 );

        no Moose;
        1;

Inheriting from that is pretty easy:

        package PerlTrainer;
        use Moose;
        extends 'Trainer';

        no Moose;
        1;

The extends keyword will load the class if needed, however it will also overwrite any previous inheritance information. Thus for multiple inheritance one must specify all parent classes in the one call to extends. For example:

        extends 'Trainer', 'Geek';

At this point our two classes are effectively identical. The PerlTrainer class automatically gets the two attributes from Trainer. To make things more interesting, let's add a review method:

        package PerlTrainer;
        use Moose;
        extends 'Trainer';

        sub review_results {
                my ($self, $material) = @_;

                if   ( $material =~ /Perl/ ) {
                        return "Awesome stuff!";
                }
                elsif( $material =~ /PERL/ ) {
                        return "An abomination!";
                }
                else {
                        return "Could be good, dunno.";
                }
        }

        no Moose;
        1;

Now, we can call that from within our parent class:

        package Trainer;
        use Moose;

        has 'name'      => ( is => 'rw', isa => 'Str', required => 1 );
        has 'subject'   => ( is => 'rw', isa => 'Str', required => 1 );
        sub review {
                my ($self, $material) = @_;

                return $self->name() . " replies: " . 
                       $self->review_results($material);
        }

        no Moose;
        1;

What if there isn't a review_results method? We should add an abstract method to our Teacher class just in case:

        sub review_results {
                my ($self) = @_;

                # Moose gives us confess (like Carp's confess) to complain
                return confess $self. " needs to be able to review materials!";
        }

Now we can create PerlTrainers and review materials:

        my $trainer = PerlTrainer->new( name => 'Paul', subject => 'Perl' );

        say $trainer->review('Programming Perl, 3rd Ed');

While this is a traditional solution, it's not necessarily a good one, as we can still create abstract objects. A much better solution is to use roles.

Roles

In the previous example, our Teacher class is essentially an abstract class. We don't really intend to create a Teacher object at any point, instead we intend to add classes to represent each type of trainer we might have. In Moose this becomes a candidate for a role.

A role never has any instances, but exists to give functionality to one or more classes. To declare a role in Moose, we use Moose::Role. Otherwise, the code is almost identical:

        package Trainer;
        use Moose::Role;

        has 'name'      => ( is => 'rw', isa => 'Str', required => 1 );
        has 'subject'   => ( is => 'rw', isa => 'Str', required => 1 );

        sub review {
                my ($self, $material) = @_;

                return $self->name(), " replies: ", 
                        $self->review_results($material);
        }

        # replace review_results sub with a requires statement:
        requires 'review_results';

        no Moose::Role;
        1;

By replacing the abstract method with a requirement that review_results exists in the classes which use this role, we can tell at compile time whether the method is going to be missing rather than waiting until runtime when the abstract subroutine gets called.

To use a role, rather than extending another class, we use with instead of extends:

        package PerlTrainer;
        use Moose;
        with 'Trainer';

        sub review_results {
                my ($self, $material) = @_;

                if   ( $material =~ /Perl/ ) {
                        return "Awesome stuff!";
                }
                elsif( $material =~ /PERL/ ) {
                        return "An abomination!";
                }
                else {
                        return "Could be good, dunno.";
                }
        }

        no Moose;
        1;

Roles as a conceptual framework

Roles work well whenever it makes more sense to say our object does a particular behaviour rather than it is a particular thing. This is handy because a number of very different kinds of things may all have certain similar behaviours. For example, coffee and cola both do caffeine, but so does caffeinated soap and caffeinated lollies. If we wanted to represent all of these, it would make sense to just have a Caffeinated role which each of these do, and let coffee and cola also extend the Beverage class if it exists.

    package Coffee;
    extends 'Beverage';
    with    'Caffeine';

    package Cola;
    extends 'Beverage';
    with    'Caffeine';
    with    'Carbonated';

    package Caffeinated::Soap;
    extends 'Soap';
    with    'Caffeine';

Once you've wrapped your mind around the idea of roles, you should find that roles make object oriented programming much easier. In fact, it's not unusual to see Moose projects which have practically discarded inheritance in favour of compositing roles, as they avoid awkward abstract classes, and provide valuable compile-time checking.

Coming next

The next Perl Tip on Moose will cover extending roles and Moose's method resolution order.

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