Commonly, you see the admonition not to operate on the members of an object, but only to work through methods. So you end up with the klunky get-change-set routine. Such primitive, 20th-century tools.

The thing is, if you have advertized that you can put a scalar in and retrieve the same scalar, you've just set in stone that, fundamentally, you've got a scalar variable. Go ahead and make it one, and let the user treat it like one. Same for an array or even a hash. If it's a settable-gettable, it's a variable and should be advertized as one.

Here's the magic in the meditation: if it's not an ordinary version of whatever data type, you can tie — and thus encapsulate — it. (Update:) This is not to say that it's always the Right Thing To Do. If you need validation performed for each store, you would have to have a separate flag to indicate error, and it would be cumbersome for the programmer. But for values that the user is free to manipulate (and especially for those that he will often update), it can be a significant improvement in convenience.

Here's some silly code to illustrate. The "password" is stored in an encrypted form in memory (presumably as a cartoonishly misguided attempt to be more secure).

package Password; use Carp; use strict; my $security = 'reallysecure'; sub TIESCALAR { my $class = shift; my $arg; return bless \$arg, $class; } sub FETCH { my $self = shift; confess "wrong type" unless ref $self; croak "usage error" if @_; return substr($$self ^ $security, 0, length($$self)); } sub STORE { my $self = shift; confess "wrong type" unless ref $self; my $newval = shift; croak "usage error" if @_; if (length($newval) < 5) { carp "Not long enough" } elsif (length($newval) > 12) { carp "Too long" } else { $$self = substr($newval ^ $security, 0, length($newval)); # print "Stored ", join('.', unpack('H2'x length($$self), $$self)), + "\n"; # print "Plain is ", join('.', unpack('H2'x length($$self), $newval +)), "\n"; } } sub encoded_form { my $self = shift; $$self; } package Main; my $foo; # This would be my object my $pwref = tie $foo->{'bar'}, 'Password'; $foo->{'bar'} = 'squamous'; print $foo->{'bar'}, "\n"; $foo->{'bar'} =~ s/am/a1m/; print $foo->{'bar'}, "\n"; print "Encoded form is ", $pwref->encoded_form, "\n";

We're not really tightening our belts, it just feels that way because we're getting fatter.

Replies are listed 'Best First'.
Re: Encapsulation without methods
by Zaxo (Archbishop) on Jun 22, 2004 at 04:02 UTC

    You may be interested in To Validate Data In Lvalue Subs. There I demonstrate a way to enforce constraints on data accessed by closures on it in lvalue subs. The key was to wedge the constraint test into assignment and friends by tieing the cloistered scalar to a 'Constraint' class.

    Don't worry too much about speed; if you have constraints that must be tested, you need correctness and completeness more than speed.

    After Compline,
    Zaxo

      It appears that several of us (you, Juerd, and I) have arrived at similar solutions to similar problems. It seems to me that Attribute::Property is the way to go, but I'll have to play with it more. I can't immediately tell whether it solves the "what if you change the implementation" challenge. I'm pretty sure your method does not.

      My intent was to address the changing-implementation issue. I am pointing out that, initially, you can use an ordinary scalar (not even tied!) if that is how your API "should" work. Later, if the implementation changes (e.g., the member needs to be stored in and retrieved from a database), you can tie it within your module, and the API doesn't change. It's a significant point that I am not advocating tie-ing every member. Only those that need to be tied.

      For validation, I think that having a set method that indicates success can be preferable to relying on error generation, depending on how reasonable you think it is for the programmer to know whether he's trying to set an illegal value. It's the difference between

      $foo->setval($val) or $foo->setval($alternative); # and something like eval { $foo->val = $val } $@ and $foo->val = $alternative;
      Something to consider.

      We're not really tightening our belts, it just feels that way because we're getting fatter.
        I am not too concerned about encapusaltion so much. I wish that there was a Tie::File::Object package. You know ... something worked like Tie::File does on arrays. Something that would make it easy for me to solve problems like I describe in Should I be using tie for this?.

        Plankton: 1% Evil, 99% Hot Gas.
Re: Encapsulation without methods
by stvn (Monsignor) on Jun 22, 2004 at 02:40 UTC

    You might want to take a look at lvalued subroutines. Not only do you keep the encapsulation of using methods, but you can treat them like scalars as well.

    package Test; sub new { my ($class, $foo, $bar) = @_; return bless { foo => $foo } => ref($class) || $class; } sub foo : lvalue { $_[0]->{foo} } package main; my $test = Test->new("foo"); print $test->foo, "\n"; $test->foo = "bar"; print $test->foo, "\n"; $test->foo =~ s/bar/baz/g; print $test->foo, "\n";

    -stvn
      perldoc perlsub says of lvalue subs:
      They violate encapsulation. A normal mutator can check the supplied ar +gument before setting the attribute it is protecting, an lvalue subro +utine never gets that chance.
      A tied scalar can check before setting. Of course, in your example, if you had tied the foo element, then you'd have encapsulation.

      We're not really tightening our belts, it just feels that way because we're getting fatter.
        They violate encapsulation. A normal mutator can check the supplied argument before setting the attribute it is protecting, an lvalue subroutine never gets that chance.

        Hmmm, for some reason, I thought that you could check that value. I guess that will teach me to post past my bedtime :)

        I guess deep down, I want them to be like C#'s properties, but alas.....

        A tied scalar can check before setting. Of course, in your example, if you had tied the foo element, then you'd have encapsulation.

        While dragonchild may be mis-understanding your idea, he is correct that tied variables are a pretty sizable performance hit, and are propbably better not used in this way, where a simple getter/setter combo will do.

        Of course, even better OO practice is to design classes with the minimum of getter/setters in the first place. Personally I only create them if and when I need them, so that I am not tempted to let it all hang out for no reason.

        -stvn
Re: Encapsulation without methods
by dragonchild (Archbishop) on Jun 22, 2004 at 02:19 UTC
    Tying a blessed object ... you get all the loss in speed for method lookup and all the loss in speed for access. Plus, it ends up being a nightmare to maintain. In my personal tests, which were highly unscientific, I ended up losing about 50% of my speed when I ended up tying my objects. Now, speed isn't everything, but 50% is a lot in my book, for a potentially negative gain in maintainability, all for what seems to be cool factor.

    ------
    We are the carpenters and bricklayers of the Information Age.

    Then there are Damian modules.... *sigh* ... that's not about being less-lazy -- that's about being on some really good drugs -- you know, there is no spoon. - flyingmoose

    I shouldn't have to say this, but any code, unless otherwise stated, is untested

      One of us is unclear on what the other is saying. I'm not sure which. I'm not tying my object, I'm including a tied scalar as a member of a hash (object). Behind the scenes, the scalar is an object (as is any tied variable), and calls methods for STORE and FETCH, which I would expect to be very comparable to SET and GET.

      We're not really tightening our belts, it just feels that way because we're getting fatter.
Benchmark: Encapsulation without methods
by Roy Johnson (Monsignor) on Jun 23, 2004 at 15:22 UTC
    I wrote a benchmark to compare tied scalar with get-and-set processing, and with mutation (passing a code ref to apply to the variable). Results are somewhat disappointing, though not necessarily prohibitive:
    Rate tie mutate mutate2 getnset naked tie 77.4/s -- -27% -36% -54% -93% mutate 107/s 38% -- -11% -37% -91% mutate2 120/s 55% 13% -- -29% -90% getnset 169/s 118% 58% 40% -- -86% naked 1181/s 1426% 1007% 882% 601% --
    naked
    is direct access to the variable. This is included for a performance upper bound.
    getnset
    is a classic two-method-call way of modifying the member
    mutate
    is a one-method-call way to get, set, or modify (via code ref) the member
    mutate2
    uses only the get and set forms of the mutate method (to illustrate the overhead of simply having the extra test)
    tie
    uses a tied scalar whose STORE and FETCH are essentially identical to the get and set methods
    The overhead for simply having the encapsulation (the difference between naked and getnset) is about a factor of 6. The overhead for using tie adds another factor of 2+. The more complex the behind-the-scenes machinations of storing and fetching, the less important that overhead will be, but it is significant.

    One note about my benchmark program: if you pass it a non-zero argument, it will pass that on to cmpthese. If you don't, it will just sample-run each method to verify that the output is sensible. Use a negative arg for seconds, as positive one for iterations.

    #! perl use strict; package Ring; use Carp; sub TIESCALAR { my $class = shift; my $arg; bless \$arg, $class; } sub FETCH { my $self = shift; confess "wrong type" unless ref $self; croak "usage error" if @_; return $$self; } sub STORE { my $self = shift; confess "wrong type" unless ref $self; my $newval = shift; croak "usage error" if @_; $$self = $newval % 12; } package MyOb; use Carp; sub new { my $class = shift; my $struct = { tie => 0, getnset => 0, mutate => 0, naked => 0 }; tie $struct->{'tie'}, 'Ring'; bless $struct, $class; } sub get { my $self = shift; confess "wrong type" unless ref $self; croak "usage error" if @_; return $self->{'getnset'}; } sub set { my $self = shift; confess "wrong type" unless ref $self; my $newval = shift; croak "usage error" if @_; $self->{'getnset'} = $newval % 12; } use UNIVERSAL 'isa'; sub mutate { my $self = shift; confess "wrong type" unless ref $self; if (@_) { my $set = shift; if (isa($set, 'CODE')) { $set->() for $self->{'mutate'}; } else { $self->{'mutate'} = $set } $self->{'mutate'} %= 12; } $self->{'mutate'}; } package Main; use Benchmark ':all'; my $arg = shift; my $ob = MyOb->new; if ($arg) { cmpthese($arg, { 'naked' => sub { $ob->{'naked'}++, $ob->{'naked'} %= 12 for 1..1 +00 }, 'tie' => sub { ++$ob->{'tie'} for 1..100 }, 'mutate' => sub { $ob->mutate(sub{++$_}) for 1..100 }, 'getnset' => sub { $ob->set($ob->get + 1) for 1..100 }, 'mutate2' => sub { $ob->mutate($ob->mutate + 1) for 1..100 }, }); } else { print 'Naked: '; &{sub { $ob->{'naked'}++, $ob->{'naked'} %= 12 for 1..100 }}; print $ob->{'naked'},"\n"; print 'Tie: '; &{sub { ++$ob->{'tie'} for 1..100 }}; print $ob->{'tie'}, "\n"; print 'Mutate: '; &{sub { $ob->mutate(sub{++$_}) for 1..100 }}; print $ob->mutate, "\n"; print 'GetNSet: '; &{sub { $ob->set($ob->get + 1) for 1..100 }}; print $ob->get, "\n"; $ob->mutate(0); print 'Mutate: '; &{sub { $ob->mutate($ob->mutate + 1) for 1..100 }}; print $ob->mutate, "\n"; }

    We're not really tightening our belts, it just feels that way because we're getting fatter.