It can be used like this:package Set; use Moo; has 'items' => (is => 'ro', default => sub { [ ] }); no Moo; sub has { my ($self, $e) = @_; scalar grep { $_ == $e } @{ $self->items }; } sub add { my ($self, $e) = @_; if ( ! $self->has($e) ) { push @{ $self->items }, $e; } } 1;
% reply 0> use Set 1> my $s = Set->new $res[0] = bless( { 'items' => [] }, 'Set' ) 2> $s->has(42) $res[1] = 0 3> $s->add(42) $res[2] = 1 4> $s->has(42) $res[3] = 1
which he can use like so:package BobUtil; use strict; use List::Util qw(min max); sub set_min { my ($set) = @_; min @{ $set->items }; } sub set_max { my ($set) = @_; max @{ $set->items }; } 1;
Bob eventually finds this usage too cumbersome and decides to make it simpler by using Inheritance to create his own set:% reply 0> use Set 1> use BobUtil 2> my $s = Set->new $res[0] = bless( { 'items' => [] }, 'Set' ) 3> $s->add($_) for 1 .. 5 $res[1] = '' 4> BobUtil::set_min($s) $res[2] = 1 5> BobUtil::set_max($s) $res[3] = 5
And now he can do this:package BobSet; use Moo; use List::Util qw(min max); extends 'Set'; no Moo; sub numeric_min { my ($self) = @_; min @{ $self->items }; } sub numeric_max { my ($self) = @_; max @{ $self->items }; } 1;
% reply 0> use BobSet 1> my $s = BobSet->new $res[0] = bless( { 'items' => [] }, 'BobSet' ) 2> $s->add($_) for 1 .. 5 $res[1] = '' 3> $s->numeric_min $res[2] = 1 4> $s->numeric_max $res[3] = 5
News of the new improved Set reaches Bob, and he installs the new version, but alas:package Set; use Moo; has 'items' => (is => 'ro', default => sub { { } }); no Moo; sub has { my ($self, $e) = @_; exists $self->items->{ $e }; } sub add { my ($self, $e) = @_; if ( ! $self->has($e) ) { $self->items->{ $e } = 1; } } 1;
And BobUtil is just as broken:% reply 0> use BobSet 1> my $s = BobSet->new $res[0] = bless( { 'items' => {} }, 'BobSet' ) 2> $s->add($_) for 1 .. 5 $res[1] = '' 3> $s->numeric_min Not an ARRAY reference at BobSet.pm line 11.
% reply 0> use Set 1> use BobUtil 2> my $s = Set->new $res[0] = bless( { 'items' => {} }, 'Set' ) 3> $s->add($_) for 1 .. 5 $res[1] = '' 4> BobUtil::set_min($s) Not an ARRAY reference at BobUtil.pm line 8.
Here the internal representation of the Set is made "private" via the "leading underscore" convention, and a public method is provided to access (a copy of) the set items (it would be better to not have an accessor for the set items, as there would be no need to rely on a convention for privacy, but this is beyond the power of Moo). And now BobSet works once again:package Set; use Moo; has '_items' => (is => 'ro', default => sub { { } }); no Moo; sub items { my ($self) = @_; [ keys %{ $self->_items } ]; } sub has { my ($self, $e) = @_; exists $self->_items->{ $e }; } sub add { my ($self, $e) = @_; if ( ! $self->has($e) ) { $self->_items->{ $e } = 1; } } 1;
% reply 0> use BobSet 1> my $s = BobSet->new $res[0] = bless( { '_items' => {} }, 'BobSet' ) 2> $s->add($_) for 1 .. 5 $res[1] = '' 3> $s->numeric_min $res[2] = '1' 4> $s->numeric_max $res[3] = '5'
The constructor generated by Moo will by default allow any attribute to be set via an "init_arg", and clearly in this case it is not desirable. There are are few ways to fix this, such as constraining the value using an "isa" directive or by an "init_arg" => undef directive.% reply 0> use Set 1> my $s = Set->new(_items => [1 .. 4]) $res[0] = bless( { '_items' => [ 1, 2, 3, 4 ] }, 'Set' ) 2> $s->add(5) Not a HASH reference at Set.pm line 15.
Yet another way is to use BUILDARGS e.g.
Now any (explicit) constructor arguments will become elements of the Set:package Set; use Moo; has '_items' => (is => 'ro', default => sub { { } }); no Moo; sub BUILDARGS { shift; return { _items => { map { $_ => 1 } @_ } }; } sub items { my ($self) = @_; [ keys %{ $self->_items } ]; } sub has { my ($self, $e) = @_; exists $self->_items->{ $e }; } sub add { my ($self, $e) = @_; if ( ! $self->has($e) ) { $self->_items->{ $e } = 1; } } 1;
% reply 0> use Set 1> my $s = Set->new(1 .. 4) $res[0] = bless( { '_items' => { '1' => 1, '2' => 1, '3' => 1, '4' => 1 } }, 'Set' ) 2> $s->has(5) $res[1] = '' 3> $s->has(3) $res[2] = 1
Here the set remembers that it has seen the value 1 twice, once via has() and once via add(), whereas it hasn't seen the value 2 via add() or has(). Chuck uses Inheritance to create this type of set% reply 0> use RememberingSet 1> $s = RememberingSet->new 3> $s->has(1) $res[1] = '' 4> $s->add(1) $res[2] = 1 5> $s->seen(1) $res[3] = 2 6> $s->seen(2) $res[4] = 0
The RememberingSet overrides the has() and add() methods in both cases updating a counter before calling the corresponding version in Alice's Set. But Chuck finds that this new set doesn't work as expectedpackage RememberingSet; use Moo; has '_count' => (is => 'ro', default => sub { { } }); extends 'Set'; no Moo; sub has { my ($self, $e) = @_; $self->_count->{ $e }++; $self->SUPER::has($e); } sub add { my ($self, $e) = @_; $self->_count->{ $e }++; $self->SUPER::add($e); } sub seen { my ($self, $e) = @_; exists $self->_count->{ $e } ? $self->_count->{ $e } : 0; } 1;
This has happened because in the Set class, the add() method calls the has() method. Chuck could fix this by not updating the count in his add() method, but this is a fragile solution as seen() would yield the wrong answer if Alice decided to update add() so that it didn't call has().% reply 0> use RememberingSet 1> my $s = RememberingSet->new $res[0] = bless( { '_count' => {}, '_items' => {} }, 'RememberingSet' ) 2> $s->has(1) $res[1] = '' 3> $s->add(1) $res[2] = 1 4> $s->seen(1) $res[3] = 3
This solution provides the expected behaviour. Composition works by wrapping the "derived" class around the original one and forwarding (or delegating) the appropriate methods to it. There are even Moosisms like "handles" and "around" that could be be used to simplify this solution e.g.package RememberingSet; use Moo; use Set; has '_count' => (is => 'ro', default => sub { { } }); has '_set' => (is => 'ro', default => sub { Set->new }); no Moo; sub has { my ($self, $e) = @_; $self->_count->{ $e }++; $self->_set->has($e); } sub add { my ($self, $e) = @_; $self->_count->{ $e }++; $self->_set->add($e); } sub seen { my ($self, $e) = @_; exists $self->_count->{ $e } ? $self->_count->{ $e } : 0; } 1;
The REPL used in the above examples is replypackage RememberingSet; use Moo; use Set; my $Delegated = [qw/add has/]; has '_count' => (is => 'ro', default => sub { { } }); has '_set' => (is => 'ro', default => sub { Set->new }, handles => $ +Delegated); around $Delegated => sub { my ($orig, $self, $e) = @_; $self->_count->{ $e }++; $self->$orig($e); }; no Moo; sub seen { my ($self, $e) = @_; exists $self->_count->{ $e } ? $self->_count->{ $e } : 0; } 1;
|
---|
Replies are listed 'Best First'. | |
---|---|
Re: OOP: How to (not) lose Encapsulation
by Athanasius (Archbishop) on May 13, 2015 at 08:24 UTC | |
Re: OOP: How to (not) lose Encapsulation
by jeffa (Bishop) on May 12, 2015 at 22:48 UTC | |
Re: OOP: How to (not) lose Encapsulation
by locked_user sundialsvc4 (Abbot) on May 12, 2015 at 20:58 UTC | |
by Anonymous Monk on May 12, 2015 at 21:08 UTC | |
by hobbs (Monk) on May 12, 2015 at 21:52 UTC | |
by Your Mother (Archbishop) on May 12, 2015 at 22:11 UTC | |
by Anonymous Monk on May 12, 2015 at 22:13 UTC | |
by Arunbear (Prior) on May 14, 2015 at 14:14 UTC | |
by locked_user sundialsvc4 (Abbot) on May 13, 2015 at 13:50 UTC |