Search code examples
perlmoose

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).


Solution

  • 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.