David B
David B

Reputation: 29998

Can I set the 'isa' of a Moose object attribute upon construction?

I have a Moose object with the following attribute:

has 'people' => (
 is      => 'ro',
 isa     => 'ArrayRef[Person::Child]',
 traits  => ['Array'],
 default => sub { [] },
 handles => {
  all_people     => 'elements',
  get_people     => 'get',
  push_people    => 'push',
  pop_people     => 'pop',
  count_people   => 'count',
  sort_people    => 'sort',
  grep_people    => 'grep',
 },
);

Note the isa is set as 'ArrayRef[Person::Child]'.

I would like to be able to choose between Person::Child, Person::Adult etc. upon creation of my object. Is that possible or must I create different objects that will be identical except the isa of the people attribute?

(This reminds me of Java generics).

Upvotes: 3

Views: 1095

Answers (2)

ajgb
ajgb

Reputation: 11

If you like Java, you might like this:

package Interfaces::Person;

use Moose::Role;

requires qw( list all attributes or methods that you require );

1;

Confirm that Person::Adult and Person::Child implement this interface:

package Person::Adult;

...
# add at the end
with qw(Interfaces::Person);

1;

and

package Person::Child;

...
# add at the end
with qw(Interfaces::Person);

1;

And back in the main class:

package My::People;
use Moose;
use MooseX::Types::Moose qw( ArrayRef );
use MooseX::Types::Implements qw( Implements );

has 'people' => (
 is      => 'ro',
 isa     => ArrayRef[Implements[qw(Interfaces::Person)]],
 traits  => ['Array'],
 default => sub { [] },
 handles => {
  all_people     => 'elements',
  get_people     => 'get',
  push_people    => 'push',
  pop_people     => 'pop',
  count_people   => 'count',
  sort_people    => 'sort',
  grep_people    => 'grep',
 },
);

And now only classes that implement Interfaces::Person interface can be added to 'people'.

Upvotes: 1

rafl
rafl

Reputation: 12341

Why not move the definition of that attribute into a role and reuse it, with the appropriate parameterisation, in other classes?

package MyApp::Thingy::HasPeople;

use MooseX::Role::Parameterized;

parameter person_type => (
    isa      => 'Str',
    required => 1,
);

role {
    my $person_type = shift->person_type;

    has 'people' => (
        is      => 'ro',
        isa     => "ArrayRef[${person_type}]",
        traits  => ['Array'],
        default => sub { [] },
        handles => {
            all_people   => 'elements',
            get_people   => 'get',
            push_people  => 'push',
            pop_people   => 'pop',
            count_people => 'count',
            sort_people  => 'sort',
            grep_people  => 'grep',
        },
    );
};

1;

and somewhere else, in the classes that actually need that attribute

package MyApp::Thingy::WithChildren;
use Moose;

with 'MyApp::Thingy::HasPeople' => { person_type => 'Person::Child' };

1;

or

package MyApp::Thingy::WithAdults;
use Moose;

with 'MyApp::Thingy::HasPeople' => { person_type => 'Person::Adult' };

1;

That way you get to both not maintain the attribute in two places, and won't end up with objects of the same class but different APIs, which tends to be a pretty big code smell.

Alternatively, you could simply write a subtype of ArrayRef that accepts either a list of either Person::Child or Person::Adult or whatever other kinds of persons you have, but only as long as all elements of that list are of the same kind.

use List::AllUtils 'all';
subtype 'PersonList', as 'ArrayRef', where {
    my $class = blessed $_->[0];
    $_->[0]->isa('Person') && all { blessed $_ eq $class } @{ $_ };
};

has persons => (
    is  => 'ro',
    isa => 'PersonList',
    ...,
);

I'd probably go for the first solution in order to be able to decide based on an objects class if it contains children, adults, or whatever.

Upvotes: 5

Related Questions