Perl Best Practices
16.4. Constructor Arguments
Pass constructor arguments as labeled values, using a hash reference. As the examples in the earlier guidelines show, when creating an object of a derived class, the initialization phase of each constructor in the class hierarchy needs to pick out the appropriate initial values for that class's attributes. This requirement makes positional arguments problematical at best, as the order in which arguments will then need to be passed to the derived constructor will depend on the order in which it inherits from its ancestral classes, as demonstrated in Example 16-3. Example 16-3. Positional arguments to constructors
package Client; use Class::Std::Utils; { my %client_num_of; sub new { my ($class, $client_num) = @_; my $new_object = bless anon_scalar( ), $class; $client_num_of{ident $new_object} = $client_num; return $new_object; } # etc. } package Client::Corporate; use base qw( Client ); use Class::Std::Utils; { my %corporation_of; sub new { my ($class, $client_num, $corp_name) = @_; my $new_object = $class->SUPER::new($client_num); $corporation_of{ident $new_object} = $corp_name; return $new_object; } # etc. } # and later... my $new_client = Client::Corporate->new( '124C1', 'Florin' );
The real problem with this approach is that any subsequent change in argument ordering (for example, adding an extra argument to either of the classes) will then require that every constructor call be rewritten, or else every derived-class constructor will have to do some sly slicing-and-dicing of the original argument list before passing it on to a base class (as in Example 16-4). Example 16-4. Adding extra positional arguments to constructors
package Client; use Class::Std::Utils; { my %client_num_of; my %name_of; # New attribute in base class sub new { # Expect extra positional argument to constructor... my ($class, $client_num, $client_name) = @_; my $new_object = bless anon_scalar( ), $class; $client_num_of{ident $new_object} = $client_num; $name_of{ident $new_object} = $client_name; return $new_object; } # etc. } package Client::Corporate; use base qw( Client ); use Class::Std::Utils; { my %corporation_of; my %position_of; # New attribute in derived class sub new { # Expect extra positional arguments to constructor... my ($class, $client_num, $corp_name, $client_name, $position) = @_; # Pass extra positional argument to base class constructor... my $new_object = $class->SUPER::new($client_num, $client_name); $corporation_of{ident $new_object} = $corp_name; $position_of{ident $new_object} = $position; return $new_object; } # etc. } # and later... my $new_client = Client::Corporate->new( '124C1', 'Florin', 'Humperdinck', 'CEO' );
Note too that it's essential to preserve the original ordering of the original positional arguments, regardless of subsequent additions to the argument list. Otherwise, you'll have to reorder the arguments in every existing constructor call in any source code that uses your class. But keeping the original arguments in their original order means that the new constructor argument lists end up with a somewhat counterintuitive interleaving of arguments for the base and derived classes. Those calls are also becoming hard to read because of the sheer number of arguments in a row[*]. [*] Is that Mr Humperdinck of Florin Corp, or Mr Florin of Humperdinck Inc??? Hashes, on the other hand, don't care what order their entries are specified in, so passing constructor arguments in a single hash is much simpler and far more change-tolerant. As Example 16-5 demonstrates, if initialization data is always passed in a hash, then each class's constructor can simply pull out of that hash those arguments it cares about, without worrying about the sequence in which the arguments were specified. Moreover, in the constructor call itself, the arguments can be specified in any convenient order. They're clearly labeled as well, which makes the code more comprehensible. Example 16-5. Avoiding positional arguments to constructors
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; $client_num_of{ident $new_object} = $arg_ref->{client_num}; $name_of{ident $new_object} = $arg_ref->{client_name}; return $new_object; } # etc. } package Client::Corporate; use base qw( Client ); use Class::Std::Utils; { my %corporation_of; my %position_of; sub new { my ($class, $arg_ref) = @_; my $new_object = $class->SUPER::new($arg_ref); $corporation_of{ident $new_object} = $arg_ref->{corp_name}; $position_of{ident $new_object} = $arg_ref->{position}; return $new_object; } # etc. } # and later... my $new_client = Client::Corporate->new( { client_num => '124C1', client_name => 'Humperdinck', corp_name => 'Florin', position => 'CEO', }); |