Perl Best Practices

16.2. Objects

Use distributed encapsulated objects.

Inside-out classes generalize very cleanly to class hierarchies, even multiple-inheritance hierarchies.

In particular, the inside-out structure neatly avoids the problem of "attribute collisions", in which both the base and derived class wish to use an attribute of the same name, but cannot successfully do so because there's only one key of that name in the object's hash.

Example 16-1 illustrates the problems of using a single, publicly accessible, collision-prone hash as your derived object. The Object class and the Psyche class each think they own the $self->{id} enTRy in each object's hash[*]. But, because that attribute isn't encapsulated, neither of them can be assured of its contents. Both classes are able to alter it at will, and the attribute is also susceptible to external tampering, as the final line of the example demonstrates.

[*] Psyche thinks the hash entry stores a complex representation of the object's primitive instincts and psychic energies, but Object reduces the same attribute to a simple integer. Orwellian programming at its double-plus-ungoodest.

The describe( ) method is a particularly disturbing piece of code in this respect. Transcribed from a genuine real-world example, it illustrates how the powerful human ability to recognize intent by context can work against a developer. Within four lines, the programmer has used $self->{id} both as the Object's ID number, and as the Psyche's id...apparently, without the slightest awareness of the fundamental contradiction that represents.

Example 16-1. Making a hash of your psyche

# Generic base class confers an ID number and description attribute # on all derived classes... package Object; # Class attribute... my $next_id = 1; # Constructor expects description as argument, # and automatically allocates ID number... sub new { my ($class, $arg_ref) = @_; # Create object representation... my $new_object = bless {}, $class; # Initialize attributes... $new_object->{ id } = $next_id++; $new_object->{desc} = $arg_ref->{desc}; return $new_object; } # and later... # Derived class for psychological modelling... package Psyche; # All instances need ID and description... use base qw( Object ); # Constructor expects to be passed an ego representation, # but generates other psychological layer automatically... sub new { my ($class, $arg_ref) = @_; # Call base-class constructor to create object representation # and initialize identity attributes... my $new_object = $class->SUPER::new($arg_ref); # Initialize psyche-specific attributes... $new_object->{super_ego} = Ego::Superstructure->new( ); $new_object->{ ego } = Ego->new($arg_ref->{ego}); $new_object->{ id } = Ego::Substrate->new( ); # Oops! Reused 'id' entry return $new_object; } # Summarize a particular psyche... sub describe { my ($self) = @_; # List case number... print "Case $self->{id}...\n"; # Describe psychological layers... $self->{super_ego}->describe( ); $self->{ ego }->describe( ); $self->{ id }->describe( ); return; } # and later still... my $psyche = Psyche->new({ desc=>'me!', ego=>'sum' }); $psyche->{id} = 'est';

Example 16-2 shows the same class hierarchy, but with each class implemented using the inside-out approach. Note that now the $id_of{ident $self} attributes of the base and derived classes no longer share a single hash entry. They're now separate entries in separate lexical hashes in separate scopes. The fact that they have the same name is now irrelevant: the methods of each class can see only the attribute that belongs to their own class.

The describe( ) method has also now been sanitized. As the Object class's $id_of{ident $self} is not in scope within the Psyche class, the only way to access it is via the public get_id( ) accessor method that Psyche inherits from the Object class. Apart from making the two "id" attributes syntactically distinct, restricting the accessibility of base case attributes in this way has the added advantage of decoupling the two classes. Psyche no longer relies on the implementation details of Object, so any aspect of the implementation of the base class could be changed without needing to modify its derived class[*] to compensate.

[*] Or, more usually, modifying all of its many derived classes.

Example 16-2. Turning your psyche inside-out

# Generic base class confers an ID number and description attribute

# on all derived classes... package Object; use Class::Std::Utils; {

# Class attribute... my $next_id = 1;

# Object attributes... my %id_of;

# ID number my %desc_of;

# Description

# Constructor expects description as argument,

# and automatically allocates ID number... sub new { my ($class, $arg_ref) = @_;

# Create object representation... my $new_object = bless anon_scalar( ), $class;

# Initialize attributes... $id_of{ident $new_object} = $next_id++; $desc_of{ident $new_object} = $arg_ref->{desc}; return $new_object; }

# Read-only access to ID number... sub get_id { my ($self) = @_; return $id_of{ident $self}; } }

# and later...

# Derived class for psychological modelling... package Psyche; use Class::Std::Utils; {

# All instances need ID and description... use base qw( Object );

# Attributes... my %super_ego_of; my %ego_of; my %id_of;

# Constructor expects to be passed an ego representation,

# but generates other psychological layers automatically... sub new { my ($class, $arg_ref) = @_;

# Call base-class constructor to create object representation

# and initialize identity attributes... my $new_object = $class->SUPER::new($arg_ref);

# Initialize psyche-specific attributes... $super_ego_of{ident $new_object} = Ego::Superstructure->new( ); $ego_of{ident $new_object} = Ego->new($arg_ref->{ego}); $id_of{ident $new_object} = Ego::Substrate->new( ); return $new_object; }

# Summarize a particular psyche... sub describe { my ($self) = @_;

# List case number... print 'Case ', $self->SUPER::get_id( ), "...\n";

# Describe pschological layers... $super_ego_of{ident $self}->describe( ); $ego_of{ident $self}->describe( ); $id_of{ident $self}->describe( ); return; } }

# and later still... my $psyche = Psyche->new({ desc=>'me!', ego=>'sum' }); $psyche->{id} = 'est';

# Exception thrown: Not a HASH reference...

Категории