Hack 46. Make Methods Really Private


Enforce encapsulation with a little more flair.

Perl's object orientation is powerful in many ways, allowing the creation and emulation of almost any kind of object or class system. It's also very permissive, enforcing no access control by default. Any code can poke and prod methods and parents into any class at any time and can call even ostensibly private methods regardless of the intent of the code's original author.

By convention, the Perl community considers methods with a leading underscore as private methods that you shouldn't override or call outside of the class or rely on any specific semantics or workings. That's usually a good policy, but there's little enforcement and it's only a convention. It's still possible to call the wrong method accidentally or even on purpose.

Fortunately, there are better (or at least scarier) ways to hide methods.

The Hack

One easy way to manipulate subroutines and methods at compile time is with subroutine attributes [Hack #45]. The Class::HideMethods module adds an attribute to methods named Hide that makes them unavailable and mostly uncallable from outside the program:

package Class::HideMethods; use strict; use warnings; use Attribute::Handlers; my %prefixes; sub import {     my ($self, $ref)      = @_;     my $package           = caller( );     $prefixes{ $package } = $ref; } sub gen_prefix {     my $invalid_chars = "\\0\\r\\n\\f\\b";     my $prefix;     for ( 1 .. 5 )     {         my $char_pos = int( rand( length( $invalid_chars ) ) );         $prefix     .= substr( $invalid_chars, $char_pos, 1 );     }     return $prefix; } package UNIVERSAL; sub Private :ATTR {     my ($package, $symbol, $referent, $attr, $data, $phase) = @_;     my $name    = *{ $symbol }{NAME};     my $newname = Class::HideMethods::gen_prefix( $package ) . $name;     my @refs    = map { *$symbol{ $_ } } qw( HASH SCALAR ARRAY GLOB );     *$symbol    = do { local *symbol };     no strict 'refs';     *{ $package . '::' . $newname } = $referent;     *{ $package . '::' . $name    } = $_ for @refs;     $prefixes{ $package }{ $name }  = $newname; } 1;

To hide the method, the code replaces the method's symbol with a new, empty typeglob. This would also delete any variables with the same name, so the code copies them out of the symbol first, and then back into the new, empty symbol. Now you know how to "delete" from a typeglob.


Running the Hack

Using this module is easy; within your class, declare a lexical hash to hold the secret new method names. Pass it to the line that uses Class::HideMethods:

package SecretClass; my %methods; use Class::HideMethods \\%methods; sub new            { bless { }, shift } sub hello :Private { return 'hello'   } sub goodbye        { return 'goodbye' } sub public_hello {     my $self  = shift;     my $hello = $methods{hello};     $self->$hello( ); } 1;

Remember to call all private methods with the $invocant->$method_name syntax, looking up the hidden method name instead.

To prove that it works, try a few tests from outside the code.

use Test::More tests => 6; my $sc = SecretClass->new( ); isa_ok( $sc, 'SecretClass' ); ok( ! $sc->can( 'hello' ),        'hello( ) should be hidden'              ); ok( $sc->can( 'public_hello' ),   'public_hello( ) should be available'    ); is( $sc->public_hello( ),     'hello', '... and should be able to call hello( )' ); ok( $sc->can( 'goodbye' ),        'goodbye( ) should be available'         ); is( $sc->goodbye( ), 'goodbye',    '... and should be callable'            );

Not even subclasses can call the methods directly. They're fairly private!

Inside the hack

Perl uses symbol tables internally to store everything with a namevariables, subroutines, methods, classes, and packages. This is for the benefit of humans. By one theory, Perl doesn't really care what the name of a method is; it's happy to call it by name, by reference, or by loose description.

That's sort of true and sort of false. Only Perl's parser cares about names. Valid identifiers start with an alphabetic character or an underscore and contain zero or more alphanumeric or underscore characters. Once Perl has parsed the program, it looks up whatever symbols it has in a manner similar to looking up values in a hash. If you can force Perl to look up a symbol containing otherwise-invalid characters, it will happily do so.

Fortunately, there's more than one way to call a method. If you have a scalar containing the name of the method (which you can define as a string containing any character, not just a valid identifier) or a reference to the method itself, Perl will invoke the method on the invocant. That's half of the trick.

The other magic is in removing the symbol from the symbol table under its unhidden name. Without this, users could bypass the hidden name and call supposedly hidden methods directly.

Without the real name being visible, the class itself needs some way to find the names of private methods. That's the purpose of the lexical %methods, which is not normally visible outside of the class itself (or at least its containing file).

Hacking the Hack

A very clever version of this code could even do away with the need for %methods in the class with hidden methods, perhaps by abusing the constant pragma to store method names appropriately.

This approach isn't complete access control, at least in the sense that the language can enforce it. It's still possible to get around this. For example, you can crawl a package's symbol table, looking for defined code. One way to thwart this is to skip installing methods back in the symbol table with mangled names. Instead, delete the method from the symbol table and store the reference in the lexical cache of methods.

That'll keep out determined people. It won't keep out really determined people who know that the PadWalker module from the CPAN lets them poke around in lexical variables outside their normal scope [Hack #76]...but anyone who wants to go to that much trouble could just as easily fake the loading of Class::HideMethods with something that doesn't delete the symbol for hidden methods. Still, it's really difficult to call these methods by accident or on purpose without some head-scratching, which is probably as good as it gets in Perl 5.



Perl Hacks
Perl Hacks: Tips & Tools for Programming, Debugging, and Surviving
ISBN: 0596526741
EAN: 2147483647
Year: 2004
Pages: 141

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net