开发者

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

开发者 https://www.devze.com 2023-01-24 14:05 出处:网络
I have a Moose object with the following attribute: has \'people\' => ( is=> \'ro\', isa=> \'ArrayRef[Person::Child]\',

I have a Moose object with the following attribute:

has 'people' => (
 is      => 'ro',
 isa     => 'ArrayRef[Person::Child]',
 traits  => ['Array'],
 default => sub { [] },
 handles => {开发者_如何学C
  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).


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.


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

0

精彩评论

暂无评论...
验证码 换一张
取 消