Beefy Boxes and Bandwidth Generously Provided by pair Networks
"be consistent"
 
PerlMonks  

OOP: How to (not) lose Encapsulation

by Arunbear (Parson)
on May 12, 2015 at 12:25 UTC ( #1126415=perlmeditation: print w/replies, xml ) Need Help??

Introduction

Moose and its smaller cousin Moo have become popular ways of creating Object Oriented code, but the existing tutorials do not have much to say about Information hiding (also known as Encapsulation), so this is an attempt to fill that gap.

Alice and the Set

Alice wants to use a Set (a collection of unique items) to keep track of data her program has already seen. She creates a Set class which initially looks like this (assume that she either hasn't learnt about hashes yet, or doesn't like the "fiddliness" of simulating a Set via a Hash):
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;
It can be used like this:
% 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

Bob, min and max

Another programmer Bob, starts using this class. Bob finds it useful but misses the ability to find the min/max element of a set, so he creates some utilities:
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;
which he can use like so:
% 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
Bob eventually finds this usage too cumbersome and decides to make it simpler by using Inheritance to create his own set:
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;
And now he can do this:
% 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

Alice updates the Set

Now realising that linear scans don't scale up as well as hash lookups, Alice decides to update her Set class to use a hash rather than an array:
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;
News of the new improved Set reaches Bob, and he installs the new version, but alas:
% 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.
And BobUtil is just as broken:
% 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.

Encapsulation lost via accessor

By making the internal representation of the Set public, anyone depending on that representation will be in trouble if the representation changes. Alice updates the Set again to correct this design error:
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;
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:
% 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'

Encapsulation lost via constructor

There's still another way in which Encapsulation is lost, consider:
% 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.
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.

Yet another way is to use BUILDARGS e.g.

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;
Now any (explicit) constructor arguments will become elements of the Set:
% 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

Encapsulation lost via Inheritance

Yet another programmer Chuck, also starts using Alice's Set class but he finds that he wants the Set to be able to remember how many times it has encountered a given value e.g. Chuck wants to create a new type of set with this behaviour:
% 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
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
package 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;
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 expected
% 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 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().

Composition

The problem with Inheritance is that it requires Chuck to know the internal workings of Alice's set class to use it correctly (thus the loss of Encapsulation). A safer form of reuse is Composition which looks like
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;
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; 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;
The REPL used in the above examples is reply

Takeaways

  • Consider making all attributes private (if only via convention)
  • Consider turning off init_args
  • Consider using Composition instead of Inheritance

Replies are listed 'Best First'.
Re: OOP: How to (not) lose Encapsulation
by Athanasius (Chancellor) on May 13, 2015 at 08:24 UTC

    Hello Arunbear,

    Thank-you and ++ for an excellent post! I think it would make a useful tutorial.

    It might be a good idea to add a little information up-front on why encapsulation is a Good Thing. First, you could note that:

    encapsulation is one of the four fundamentals of OOP
    Wikipedia, “Encapsulation”

    (The others being abstraction, inheritance, and polymorphism.) Then, you could use the following quote from Scott Meyers (one of the gurus of C++) to show why encapsulation is valuable:

    Encapsulation is a means, not an end. There’s nothing inherently desirable about encapsulation. Encapsulation is useful only because it yields other things in our software that we care about. In particular, it yields flexibility and robustness.
    ...
    This is the real problem with poor encapsulation: it precludes future implementation changes. Unencapsulated software is inflexible, and as a result, it’s not very robust. When the world changes, the software is unable to gracefully change with it.
    — “How Non-Member Functions Improve Encapsulation,” Dr. Dobb’s Journal (1st February, 2000)

    That’s an article I would recommend to anyone interested in OO.

    Finally, you should perhaps point out that OOP is a programming paradigm, or methodology, and as such should not be confused with “OO language features” (such as bless) which are provided to facilitate implementation of the paradigm. It is possible to write an OO programme in straight C (I’ve seen it done very competently), and all-too-easy to write non-OO programmes in “pure OO” languages such as Java. Emphasising this distinction should help to forestall objections from those who find encapsulation to be “restrictive and prohibitive” — by pointing out that they are entirely free to use bless, etc., in any way they choose; but that when they choose to do so in a way which eschews encapsulation, the code they produce is, by definition, not OO.

    Hope that helps,

    Athanasius <°(((><contra mundum Iustus alius egestas vitae, eros Piratica,

Re: OOP: How to (not) lose Encapsulation
by jeffa (Bishop) on May 12, 2015 at 22:48 UTC

    "Composition_over_inheritance" was the mantra that i learned back in 1998 when i attended my first advanced OO class in college. The bad news is that it took me nearly 10 years to understand. :) The good news is that packages like Moose make all of this easier to implement than not. Need to define an interface?

    package My::Abstract::Interface; use Moose::Role; requires qw( foo bar baz qux );
    instead of
    package My::Abstract::Interface; use Carp; sub foo { croak } sub bar { croak } sub baz { croak } sub qux { croak }
    If you think about, this is a form of encapsulation itself -- by making it easier to define a proper abstract interface, coders are more tempted to pick the easier-to-code-and-maintain version. Moose (et. al.) make it very easy to delegate and aggregate, without resorting to inheritance:
    #!/usr/bin/env perl package Foo; use Moose; has string => ( is => 'rw', isa => 'Str' ); sub to_string { shift->string } package Bar; use Moose; has _foo => ( is => 'rw', isa => 'Foo', handles => ['string'] ); around BUILDARGS => sub {my($o,$c)=(shift,shift);$c->$o(_foo => Foo->n +ew(@_))}; sub to_string { uc shift->string } package main; my $foo = Foo->new( string => 'hello world' ); print $foo->to_string, $/; my $bar = Bar->new( %$foo ); print $bar->to_string, $/;
    For me, this is actually easier than using inheritance with classic Perl OO.

    UPDATE:
    Changed arguments to Bar constructor. (Was the same as arguments to Foo constructor.)

    jeffa

    L-LL-L--L-LL-L--L-LL-L--
    -R--R-RR-R--R-RR-R--R-RR
    B--B--B--B--B--B--B--B--
    H---H---H---H---H---H---
    (the triplet paradiddle with high-hat)
    
Re: OOP: How to (not) lose Encapsulation
by sundialsvc4 (Abbot) on May 12, 2015 at 20:58 UTC

    A splendid, and thorough, treatment of this subject ... “specific to Moose and Perl, and yet, not at all.”   Excellent, pragmatic examples and use-cases to be gleaned here for programmers using any language.   Superb pointing-out of the particular features of Moo/Moose and how/when/why one would use them.   You should be writing textbooks.   Thank you for sharing this Meditation.   “++, of course.”

      Yeah well I feel the opposite. I use Perl OO because I do NOT want such strict encapsulation. I find this entire methodology to be restrictive and prohibitive. I got away from the "OOPolice," maybe it is not too late for you to make a jail break too.
        There's no dogma here, just a set of well-worked examples of what could go wrong with code reuse in certain scenarios, how encapsulation can help, and honestly, how it can also make things more complicated. Is there any particular reason you chose to resort to mindless name-calling?

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://1126415]
Approved by Corion
Front-paged by Athanasius
help
Chatterbox?
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others chanting in the Monastery: (6)
As of 2017-11-21 01:57 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    In order to be able to say "I know Perl", you must have:













    Results (294 votes). Check out past polls.

    Notices?