1

What if I want a role to modify a method if the consuming class does not have it, or provide a default method where the consuming class does not?

In one case, using a method modifier works. In the other case, just defining an ordinary method works. Is there a method that works in both cases?

Concrete example:

package UsualFavorites;
use Moose::Role;

around favorite_things {
    my ($self, $orig) = @_;
    $self->$orig(), qw/doorbells sleighbells/;
}

If the consuming class does not define a favorite_things method, I want it to end up with a favorite_things method that just returns (doorbells, sleighbells).

gcbenison
  • 11,723
  • 4
  • 44
  • 82

3 Answers3

3

You could do this using MooseX::Role::Parameterized:

Favorites.pm

package Favorites;

use MooseX::Role::Parameterized;

parameter method_name => (
    isa     => 'Str',
    default => 'favorite_things'
);

role {
    my $p = shift;
    my %args = @_;
    my $consumer = $args{consumer};

    my $method_name = $p->method_name;
    my @default_values = qw/doorbells sleighbells/;

    if ( $consumer->find_method_by_name($method_name) ) {
        around $method_name => sub {
            my $orig = shift;
            my $self = shift;

            $self->$orig(@_), @default_values;
        };
    }
    else {
        method $method_name => sub {
            my $self = shift;

            return @default_values;
        };
    }
};

no Moose::Role;

1;

Santa.pm (Santa likes doorbells, right?):

package Santa;

use Moose;
use namespace::autoclean;

with 'Favorites';

__PACKAGE__->meta->make_immutable;

1;

ACDC.pm

package ACDC;

use Moose;
use namespace::autoclean;

with 'Favorites';

sub favorite_things {
    my $self = shift;

    return 'Hells Bells';
}

__PACKAGE__->meta->make_immutable;

1;

favorites_test

use strict;
use warnings;
use 5.010;

use ACDC;
use Santa;

my $kris_kringle = Santa->new;
say 'Santa likes ', join(', ', $kris_kringle->favorite_things);

my $acdc = ACDC->new;
say 'AC/DC likes ', join(', ', $acdc->favorite_things);

Output:

Santa likes doorbells, sleighbells
AC/DC likes Hells Bells, doorbells, sleighbells

Note that you have to do additional gymnastics if your role is consumed by another parameterized role, or if your role is applied to an object instance. Ether describes both of these cases in How can I access the meta class of the module my Moose role is being applied to? and notes in a comment that:

I no longer consider the above a "best practice", and indeed have refactored out all of this (ab)use of MXRP. IMHO if you need to access $meta from within a role, you have something stinky in your design.

Is there any reason you can't simply make favorite_things required?

Community
  • 1
  • 1
ThisSuitIsBlackNot
  • 23,492
  • 9
  • 63
  • 110
3

Just define the method in the role. If the class has a method with the same name then the method from the role will be ignored.

package UsualFavorites;
use Moose::Role;

sub favorite_things {
    return ();
}
around favorite_things => sub {
    my ($orig, $self) = @_;
    return ($self->$orig(), qw/doorbells sleighbells/);
};

package Consumer;
use Moose;
with 'UsualFavorites';

sub favorite_things {
    return qw/shipbells/;
}
ThisSuitIsBlackNot
  • 23,492
  • 9
  • 63
  • 110
Denis Ibaev
  • 2,470
  • 23
  • 29
  • Wow, *much* simpler than my hack. Re. "If the class has a method with the same name then the method from the role will be ignored." Is this documented somewhere? – ThisSuitIsBlackNot Apr 24 '15 at 21:22
  • 1
    @ThisSuitIsBlackNot I don’t know. But see http://metacpan.org/source/ETHER/Moose-2.1404/lib/Moose/Meta/Role/Application/ToClass.pm#L133 – Denis Ibaev Apr 24 '15 at 22:05
1

Taking ThisSuitIsBlackNot's solution and simplifying a bit, I have:

package UsualFavorites;
use Moose::Role;
use strict;
use warnings;

around favorite_things => sub {
    my ($orig, $self) = @_;
    $self->$orig(), qw/doorbells sleighbells/;
};

sub favorite_things { () }

package Santa;

use Moose;
use strict;
use warnings;
with 'UsualFavorites';

package ACDC;

use Moose;
use strict;
use warnings;
with 'UsualFavorites';

sub favorite_things {
    my $self = shift;
    return 'Hells Bells';
}

package main;

use strict;
use warnings;
use 5.010;

my $kris_kringle = Santa->new;
say 'Santa likes ', join(', ', $kris_kringle->favorite_things);

my $acdc = ACDC->new;
say 'AC/DC likes ', join(', ', $acdc->favorite_things);

So I both have the around and I have the default implementation in the role, and it seems to work.

Tanktalus
  • 21,664
  • 5
  • 41
  • 68
  • Nice, much cleaner than my approach. This is also what [Denis Ibaev suggested](http://stackoverflow.com/a/29856616/176646). As far as the "seems to work," Denis said that "If the class has a method with the same name then the method from the role will be ignored" but I can't find this in the documentation anywhere. Do you know if this behavior is documented? – ThisSuitIsBlackNot Apr 24 '15 at 21:48
  • This is just simple, regular old inheritance. Since the Role is a parent class, the child class is just overriding it. And could call `$self->SUPER::favorite_things` as well, though there's no need here. – Tanktalus Apr 25 '15 at 14:42
  • I don't think that's right...roles are not parent classes. You can see the inheritance relationships by calling `say for __PACKAGE__->meta->linearized_isa;` inside the consuming class; the name of the role is not returned. The reason `$self->SUPER::favorite_things` works is because it resolves to the method in the current package (which happens to be named `favorite_things`), not the method in the role. Create a `foo` method in the role and call `$self->SUPER::foo` in the consuming class; you will get `Can't locate object method "foo" via package "Consumer"` – ThisSuitIsBlackNot Apr 26 '15 at 14:02
  • Denis Ibaev [pointed out](http://stackoverflow.com/questions/29852476/role-that-modifies-or-provides-a-method#comment47842552_29856616) how methods from roles are applied to classes; indeed, methods are skipped if they are already defined in the consuming class: `next if $class_method && $class_method->body != $method->body;` – ThisSuitIsBlackNot Apr 26 '15 at 14:08