Reputation: 29998
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
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
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