Beefy Boxes and Bandwidth Generously Provided by pair Networks Cowboy Neal with Hat
Clear questions and runnable code
get the best and fastest answer
 
PerlMonks  

Prevent direct acces to object's attributes

by vitoco (Pilgrim)
on Sep 07, 2009 at 16:27 UTC ( #793984=perlquestion: print w/ replies, xml ) Need Help??
vitoco has asked for the wisdom of the Perl Monks concerning the following question:

I have a module where the constructor is something like:

sub new { my $class = shift; my $self = {}; $self->{atr1} = undef; $self->{atr2} = undef; # more attributes... bless($self, $class); # ... some code to populate attribs from passed parms return $self; }

I can create an object with:

use MyMod; my $mm = MyMod->new(atr1 => 1, atr2 => 'string2');

I can get any attribute value directly with $mm->{atr1} and also set it with a simple assignment.

As some validations/actions are required before/after an attribute is set, I want to prevent this type of access, and allow it only through get() and set() methods also provided for the class.

Is this possible? Or am I doing something wrong?

Comment on Prevent direct acces to object's attributes
Select or Download Code
Re: Prevent direct acces to object's attributes
by Corion (Pope) on Sep 07, 2009 at 16:36 UTC

    This is possible, but you're doing something wrong if you want to do this.

    The so-called "inside-out" objects are a solution to this perceived problem, InsideOut is a search that gives you at least Object::InsideOut and Class::InsideOut, and likely many more implementations.

    The Cool Train has left inside-out land about two years ago and all the cool kids use Moose nowadays. At least in respect towards interoperability, Moose-based objects are less problematic than inside-out objects.

    Why do you want to prevent access to object values using $obj->{...}? Maybe you want to educate your project members to not do that? For example Data::Dumper won't work with inside-out objects...

      At least in respect towards interoperability, Moose-based objects are less problematic than inside-out objects.
      That statement surprises me. Classes implemented as inside out object do not enforce any restrictions on classes that inherit from them, and they can inherit from any class, regardless of how it's implemented.

        Yeah - on second thought the distinction isn't as grave as I made it out to be. In fact, I have used that transparent property of inside-out objects myself to "attach" data to foreign objects that didn't like their data structure modified.

        If an object already is an inside-out object, you have no choice but to continue down that road. The same holds true for Moose-based (hash) objects.

        And in the reverse, if you have a hash-based object, you can glue inside-out extensions to it, and it seems as if you can derive a Moose-based class from it.

      Why do you want to prevent access to object values using $obj->{...}? Maybe you want to educate your project members to not do that?

      Reading directly from attributes is not a real problem, but writing is, because there are some interdependencies between attributes: some of them are preprocessed values based on other ones (like a cache), and changing them would produce unexpected behavior on some methods.

      Of course, that is mentioned in the POD, but then I thought I missed something from the docs.

      I'm new in writing OO code... Now, Moose and InsideOut objects are in my queue for further reading, just after Damian Conway's book!

      Thanks to everyone...

        You could have a look at the lock functions in Hash::Util. It's a core module since 5.8, so there's probably nothing to install.

        But, I'm sure I've read somewhere that their use is discouraged in objects. Can any Wise Old Monks shed any light on that?

        Just Use Moose. You'll love it.

Re: Prevent direct acces to object's attributes
by graff (Chancellor) on Sep 07, 2009 at 16:47 UTC
    Why would it not be sufficient to just make it clear in your module's POD that direct access to object-internal values is strongly discouraged, and that the get() and set() methods should always be used? Are you really expecting that end-users would deliberately set out to do things that are bad for them?

    A common convention that is used to reinforce this "social engineering" aspect of perl 5's OO is to prefix your "internal" variable names with an underscore character ("_atr1", "_atr2", etc).

    If you don't have Damian Conway's fine book, Objected Oriented Perl, you should get a copy. Chapter 11, "Encapsulation", explains what you need to know and do to use closures inside your classes so that direct access is effectively ruled out. I'm no expert on this myself, and it would take me a while to demonstrate -- perhaps another monk will step in...

      Are you really expecting that end-users would deliberately set out to do things that are bad for them?

      I guess that's the very personality model that some other languages (e.g. Java) hold of their programmers/end-users... ;)

        And isn't that exactly why some --such as me-- consider Java to be evil?

        CountZero

        A program should be light and agile, its subroutines connected like a string of pearls. The spirit and intent of the program should be retained throughout. There should be neither too little or too much, neither needless loops nor useless variables, neither lack of structure nor overwhelming rigidity." - The Tao of Programming, 4.1 - Geoffrey James

Re: Prevent direct acces to object's attributes
by shmem (Canon) on Sep 07, 2009 at 21:12 UTC

    There's a nifty module on CPAN which ensures that object attributes can only be retrieved and altered inside the objects' class: Alter by Anno. Sample:

    package Foo; use Alter ego => {}; my @attr = qw ( bar quux ); sub new { my $class = shift; my $obj = {}; bless $obj, $class; if ( @_) { @_ % 2 and die "odd number of arguments, aborted"; my %args = @_; my %params; for (@attr) { $params{ $_} = delete $args{ $_}; } warn "unknown key $_!\n" for keys %args; for ( keys %params) { $obj->$_( $params{ $_}); } } return $obj; } sub bar { my $obj = shift; ego( $obj)->{ bar} = shift if @_; return ego( $obj)->{ bar}; } sub quux { my $obj = shift; ego( $obj)->{ quux} = shift if @_; ego( $obj)->{ quux}; } package main; print "\nOutput:\n" my $foo = Foo->new( foo => 1, bar => 2, quux => 3); print "\$foo reference is '$foo'\n"; $foo->{bar} = "something"; # hash lookup $foo->bar(5); # method print "method: \$foo->bar( ) = ", $foo->bar( ), "\n"; print "hash: \$foo->{ bar} = ", $foo->{ bar}, "\n"; __END__ Output: unknown key foo! $foo reference is 'Foo=HASH(0x8a007a4)' method: $foo->bar( ) = 5 hash: $foo->{ bar} = something

    Here, a Foo object is just a blessed hash reference. Outside its class it behaves just as a normal hash reference. Its Foo attributes can be retrieved or set only via methods.

    Note that the attribute container doesn't need to be of the same type as the object. The object could be a scalar reference (or an anonymous subroutine) and still have a hash magically attached.

Re: Prevent direct acces to object's attributes
by cdarke (Prior) on Sep 08, 2009 at 08:54 UTC
    Here is your module implemented as an inside-out class:
    package MyMod; use strict; use warnings; use Carp; use Scalar::Util qw(refaddr); # Have a private hash for each attribute my %atr1; # Attribute 1 my %atr2; # Attribute 2 # etc. # This hash is used by the general purpose accessors my %hashrefs = ( atr1 => \%atr1, atr2 => \%atr2, #etc ); sub new { my $class = shift; my %args = @_; # create some unique number from a reference my $self = \do{my $dummy}; my $key = refaddr($self); $atr1{$key} = $args{'atr1'}; $atr2{$key} = $args{'atr2'}; # more attributes... bless($self, $class); # ... some code to populate attribs from passed parms return $self; } # General purpose get/set sub set { my ($self, $attr, $value) = @_; my $key = refaddr $self; if (exists $hashrefs{$attr} ) { $hashrefs{$attr}{$key} = $value } else { carp "Invalid attribute name $attr" } } sub get { my ($self, $attr) = @_; my $key = refaddr $self; if (exists $hashrefs{$attr} ) { return $hashrefs{$attr}{$key} } else { carp "Invalid attribute name $attr"; return; } } # MUST have a destructor sub DESTROY { my ($self) = @_; my $key = refaddr $self; delete $atr1{$key}; delete $atr2{$key}; # likewise for other attribute hashes } 1;
    And here is the user code:
    use strict; use warnings; use MyMod; my $mm = MyMod->new(atr1 => 1, atr2 => 'string2'); print $mm->get('atr1')."\n"; print $mm->get('atr2')."\n"; $mm->set('atr1', 42); print $mm->get('atr1')."\n"; $mm->{atr1} = 3000; <<<<---- Fails "Not a HASH reference"
      Your implementation is not going to work in a threaded environment, as memory addresses will change after a clone. You will have to implement a CLONE method as well.

      Or you could just use 5.10.x and use Hash::Util::FieldHash::fieldhash. Then the work needed in CLONE and DESTROY will be done for you, and you don't need refaddr either. Just use the reference itself as the hashkey.

      Hash::Util::FieldHash makes Inside-Out Objects much easier to work with. With it, I would write your example as:

      package MyMod; use strict; use warnings; use Carp; use Hash::Util::FieldHash qw[fieldhash]; fieldhash my %attr1; fieldhash my %attr2; my %hashrefs = ( attr1 => \%attr1, attr2 => \%attr2, ); sub new { my $class = shift; my %args = @_; my $self = bless do {\my $dummy}, $class; $attr1{$self} = $args{attr1}; $attr2{$self} = $args{attr2}; $self; } sub set { my ($self, $attr, $value) = @_; if (exists $hashrefs{$attr}) { $hashrefs{$attr}{$self} = $value } else { carp "Invalid attribute name $attr"; } } sub get { my ($self, $attr) = @_; if (exists $hashrefs{$attr}) { return $hashrefs{$attr}{$self} } else { carp "Invalid attribute name $attr"; return; } } 1;

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://793984]
Approved by graff
Front-paged by Arunbear
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others perusing the Monastery: (9)
As of 2014-04-23 19:12 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    April first is:







    Results (553 votes), past polls