Search code examples
perlmoose

Moo(se), before and inheritance


I have a Moo(se)[0] class with a number of methods which have the exact same type of "guard statement" at the top. Instead of writing the same code several of times I figured I could put the statement in a "before" method modifier, and that works perfectly. Unless this class is subclassed, because then the "before guard" is never called.

package Foo;
use feature 'say';
use Moose;

has '_init' => (
  is      => 'rw',
  isa     => 'Bool',
  default => 0
);

sub init {
  shift->_init(1);
}

sub method {
  say "in Foo::method";
}

before method => sub {
  my $self = shift;
  warn "==> Foo is not initialized\n" unless $self->_init;
};


package Bar;
use feature 'say';
use Moose;
extends 'Foo';

sub method {
  say "in Bar::method";
}


package main;
use feature 'say';

my $foo = Foo->new;
say "foo the wrong way:";
$foo->method;

say "foo the right way:";
$foo->init;
$foo->method;

my $bar = Bar->new;
say "bar the wrong way:";
$bar->method;

Output is then (with some added new lines):

foo the wrong way:
==> Foo is not initialized
in Foo::method

foo the right way:
in Foo::method

bar the wrong way:
in Bar::method

I assume this behaviour is by design, but is there any (nice) way to make sure all subclasses also inherit the "before" method modifier/guard statement? Or is there a different way to accomplish this (I suspect it's a rather common construct). Note that an exception will be thrown in the real guard statement, but a "warn" is much simpler in example code.

[0] I prefer to use Moo because I don't use any features requiring MOP, but both Moo and Moose works the exact same way in this matter.

Edit using Roles.

If a add a Role for this (as suggested by tobyink), and add another method for making things a bit more 'real life', I get a peculiar result.

package Warning::NotInit;
use feature 'say';
use Moose::Role;

has '_init' => (is => 'rw', isa => 'Bool', default => 0);

before qw/ m1 m2 / => sub {
  my $self  = shift;
  my $class = ref($self);
  warn "==> $class is not initialized\n" unless $self->_init;
};

package Foo;
use feature 'say';
use Moose;
with 'Warning::NotInit';

sub init { shift->_init(1) }
sub m1   { say "in Foo::m1" }
sub m2   { say "in Foo::m2" }

package Bar;
use feature 'say';
use Moose;
extends 'Foo';
with 'Warning::NotInit';

sub m1 { say "in Bar::m1" }


package main;
use feature 'say';

When calling the not overridden method in the subclass, the before method is called twice.

my $bar = Bar->new;
say "bar the wrong way:";
$bar->m1;
$bar->m2;

Output:

bar the wrong way:
==> Bar is not initialized
in Bar::m1

==> Bar is not initialized
==> Bar is not initialized
in Foo::m2

Why is it called twice?


Solution

  • Yes, that's not how method modifiers work. The before modifier becomes part of the method itself. When you override the method in your subclass, you're overriding the entirety of the superclass' behaviour - that is, you're overriding the method modifier as well.

    You could solve this by factoring out the method modifier into a role which can be applied to each class, like this:

    package Warning::NotInit;
    use feature 'say';
    use Moose::Role;
    
    has '_init' => (
      is      => 'rw',
      isa     => 'Bool',
      default => 0
    );
    
    before method => sub {
      my $self  = shift;
      my $class = ref($self);
      warn "==> $class is not initialized\n" unless $self->_init;
    };
    
    package Foo;
    use feature 'say';
    use Moose;
    with 'Warning::NotInit';
    
    sub init {
      shift->_init(1);
    }
    
    sub method {
      say "in Foo::method";
    }
    
    package Bar;
    use feature 'say';
    use Moose;
    extends 'Foo';
    with 'Warning::NotInit';
    
    sub method {
      say "in Bar::method";
    }
    
    
    package main;
    use feature 'say';
    
    my $foo = Foo->new;
    say "foo the wrong way:";
    $foo->method;
    
    say "foo the right way:";
    $foo->init;
    $foo->method;
    
    my $bar = Bar->new;
    say "bar the wrong way:";
    $bar->method;