Perl Best Practices

16.5. Base Class Initialization

Distinguish arguments for base classes by class name as well.

As explained earlier, one of the great advantages of using inside-out classes instead of hashes is that a base class and a derived class can then each have an attribute of exactly the same name. In a single-level hash, that's impossible.

But that very fact also presents something of a problem when constructor arguments are themselves passed by hash. If two or more classes in the name hierarchy do happen to have attributes of the same name, the constructor will need two or more initializers with the name keywhich a single hash can't provide.

The solution is to allow initializer values to be partitioned into distinct sets, each uniquely named, which are then passed to the appropriate base class. The easiest way to accomplish that is to pass in a hash of hashes, where each top-level key is the name of one of the base classes, and the corresponding value is a hash of initializers specifically for that base class. Example 16-6 shows how this can be achieved.

Example 16-6. Avoiding name collisions in constructor arguments

package Client; use Class::Std::Utils; { my %client_num_of;

# Every client has an ID number my %name_of; sub new { my ($class, $arg_ref) = @_; my $new_object = bless anon_scalar( ), $class;

# Initialize this class's attributes with the appropriate argument set... $client_num_of{ident $new_object} = $arg_ref->{'Client'}{client_num}; $name_of{ident $new_object} = $arg_ref->{'Client'}{client_name}; return $new_object; } } package Client::Corporate; use base qw( Client ); use Class::Std::Utils; { my %client_num_of;

# Corporate clients have an additional ID number my %corporation_of; my %position_of; sub new { my ($class, $arg_ref) = @_; my $new_object = $class->SUPER::new($arg_ref); my $ident = ident($new_object);

# Initialize this class's attributes with the appropriate argument set... $client_num_of{$ident} = $arg_ref->{'Client::Corporate'}{client_num}; $corporation_of{$ident} = $arg_ref->{'Client::Corporate'}{corp_name}; $position_of{$ident} = $arg_ref->{'Client::Corporate'}{position}; return $new_object; } }

# and later... my $new_client = Client::Corporate->new( { 'Client' => { client_num => '124C1', client_name => 'Humperdinck', }, 'Client::Corporate' => { client_num => 'F_1692', corp_name => 'Florin', position => 'CEO', }, });

Now each class's constructor picks out the initializer subhash whose key is that class's own name. Because every class name is different, the top-level keys of this multilevel initializer hash are guaranteed to be unique. And because no single class can have two identically named attributes, the keys of each second-level hash will be unique as well. If two classes in the hierarchy both need an initializer of the same name (e.g., 'client_num'), those two hash entries will now be in separate subhashes, so they will never clash.

A more sophisticated variationwhich is generally much more convenient for the users of your classis to allow both general and class-specific initializers in your top-level hash, as demonstrated in Example 16-7.

Example 16-7. More flexible initializer sets

package Client; use Class::Std::Utils; { my %client_num_of; my %name_of; sub new { my ($class, $arg_ref) = @_; my $new_object = bless anon_scalar( ), $class;

# Initialize this class's attributes with the appropriate argument set... my %init = extract_initializers_from($arg_ref); $client_num_of{ident $new_object} = $init{client_num}; $name_of{ident $new_object} = $init{client_name}; return $new_object; }

# etc. } package Client::Corporate; use base qw( Client ); use Class::Std::Utils; { my %client_num_of; my %corporation_of; my %position_of; sub new { my ($class, $arg_ref) = @_; my $new_object = $class->SUPER::new($arg_ref); my $ident = ident($new_object);

# Initialize this class's attributes with the appropriate argument set... my %init = extract_initializers_from($arg_ref); $client_num_of{$ident} = $init{client_num}; $corporation_of{$ident} = $init{corp_name}; $position_of{$ident} = $init{position}; return $new_object; }

# etc. }

In this version of the classes, clients don't need to specify classnames for initializers unless the names of those initializers actually are ambiguous:

my $new_client = Client::Corporate->new( { client_name => 'Humperdinck', corp_name => 'Florin', position => 'CEO', 'Client' => { client_num => '124C1' }, 'Client::Corporate' => { client_num => 'F_1692' }, });

Any other arguments can just be passed directly in the top-level hash. This convenience is provided by the exTRact_initializers_from( ) utility method (which is exported from the Class::Std::Util CPAN module):

sub extract_initializers_from { my ($arg_ref) = @_;

# Which class are we extracting arguments for? my $class_name = caller;

# Find the class-specific sub-hash (if any)... my $specific_inits_ref = first {defined $_} $arg_ref->{$class_name}, {}; croak "$class_name initializer must be a nested hash" if ref $specific_inits_ref ne 'HASH';

# Return initializers, overriding general initializers from the top level

# with any second-level initializers that are specific to the class.... return ( %{$arg_ref}, %{$specific_inits_ref} ); }

The subroutine is always called with the original multilevel argument set ($arg_ref) from the constructor. It then looks up the class's own name in the argument set hash, to see if an initializer with that key has been defined (i.e., $arg_ref->{$class_name}). If none has, an empty hash ({}) is used instead. Either way, the resulting set of class-specific initializers ($specific_inits_ref) is then checked, to make sure it's a genuine (sub)hash.

Finally, extract_initializers_from( ) returns the flattened set of key/value pairs for the class's initializer set, by appending the class-specific initializer set (%{$specific_inits_ref}) to the end of the original generic initializer set (%{$arg_ref}). Appending the specific initializers after the generic ones means that any key in the class-specific set will override any key in the generic set, thereby ensuring that the most relevant initializers are always selected, but that generic initializers are still available where no class-specific value has been passed in.

The only drawback of using hash-based initialization is that you re-introduce the possibility that misspelling an attribute name will result in mis-initialization. For example, the following constructor call would correctly initialize everything except the client name:

my $new_client = Client::Corporate->new( { calient_name => 'Humperdinck',

# Diantre

! corp_name => 'Florin', position => 'CEO', 'Client' => { client_num => '124C1' }, 'Client::Corporate' => { client_num => 'F_1692' }, });

There are two straightforward solutions to this problem. The first is radical: prohibit initialization completely. That is, every constructor is implemented as:

sub new { my ($class) = @_; croak q{Can't initialize in constructor (use accessors)} if @_ > 1;

# [Set up purely internal state here] return bless anon_scalar( ), $class; }

This style forces every object to be initialized through its standard accessor mechanisms:

my $new_client = Client::Corporate->new( ); $new_client->set_client_name('Humperdinck'); $new_client->set_corp_name ('Florin'); $new_client->set_position('CEO'); $new_client->Client::set_client_num ('124C1'); $new_client->Client::Corporate::set_client_num('F_1692');

Most people find this approach inconvenient, unless they set up each set_... accessor so that it returns its own $self value. For example:

sub set_client_name { my ($self, $new_name) = @_; $name_of{ident $self} = $new_name; return $self; }

If every set_... accessor is built that way, they can then be chained during initializations:

my $new_client = Client::Corporate->new( ) -> set_client_name('Humperdinck') -> set_corp_name('Florin') -> set_position('CEO') -> Client::set_client_num('124C1') -> Client::Corporate::set_client_num('F_1692') ;

An alternative solution that does allow initializer values to be passed to the constructor, but still ensures that every attribute is correctly initialized, is described under "Attribute Building" later in this chapter.

Категории