Документация
HTML CSS PHP PERL другое

Section 16.6.  Construction and Destruction

 
Previous
Table of Contents
Next

16.6. Construction and Destruction

Separate your construction, initialization, and destruction processes.

Classes that use a single new( ) method to both create and initialize objects usually don't work well under multiple inheritance. When a class hierarchy offers two or more new( ) methods (either at different inheritance levels, or in different base classes at the same level), then there is automatically a conflict of control.

Only one of those new( ) methods can ultimately allocate and bless the storage for the new object, and if there is multiple inheritance anywhere in the class hierarchy you're using, the new( ) chosen may not be the new( ) you expected. Even if it is the one you wanted, any constructors on other branches of the inheritance tree will have been pre-empted and the object will not be completely initialized.

Likewise, when the object's destructors are called, only one of the two or more inheritance branches can be followed during destructor look-up, so only one of the several base-class destructors will ever be called. That's particularly bad, because it's critical to call all the destructors of an inside-out object, to ensure that its attribute hashes don't leak memory (see "Destructors" in Chapter 15).

For example, you could create a well-implemented inside-out class like this:


    package Wax::Floor;
    use Class::Std::Utils;
    {
        
# Attributes...
my %name_of; my %patent_of; sub new { my ($class, $arg_ref) = @_;
my %init = extract_initializers_from($arg_ref); my $new_object = bless anon_scalar( ), $class; $name_of{ident $new_object} = $init{name}; $patent_of{ident $new_object} = $init{patent}; return $new_object; } sub DESTROY { my ($self) = @_; delete $name_of{ident $self}; delete $patent_of{ident $self}; return; } }

and a second class such as:


    package Topping::Dessert;
    use Class::Std::Utils;
    {
        
# Attributes...
my %name_of; my %flavour_of; sub new { my ($class, $arg_ref) = @_;
my %init = extract_initializers_from($arg_ref); my $new_object = bless anon_scalar( ), $class; $name_of{ident $new_object} = $init{name}; $flavour_of{ident $new_object} = $init{flavour}; return $new_object; } sub DESTROY { my ($self) = @_; delete $name_of{ident $self}; delete $flavour_of{ident $self}; return; } }

But it's impossible to create a class that correctly inherits from both. The closest you can get is the class shown in Example 16-8. And it still fails dismally.

Example 16-8. When multiple inheritance attacks
package Shimmer;
use base qw( Wax::Floor  Topping::Dessert );
use Class::Std::Utils;
{
    # Attributes...
    my %name_of;
    my %patent_of;

    sub new {
        my ($class, $arg_ref) = @_;

        my %init = extract_initializers_from($arg_ref);

        # Call base-class constructor to allocate and pre-initialize...
        my $new_object = $class->SUPER::new($arg_ref);

        $name_of{ident $new_object}   = $init{name};
        $patent_of{ident $new_object} = $init{patent};

        return $new_object;
    }

    sub DESTROY {
        my ($self) = @_;

        delete $name_of{ident $self};
        delete $patent_of{ident $self};

        # Call base-class destructor to continue clean-up...
        $self->SUPER::DESTROY( );

        return;
    }
}

In the Shimmer constructor, the nested call to the ancestral constructor ($class->SUPER::new($arg_ref)) will find only the left-most ancestral new( ). So Wax::Floor::new( ) will be called, and will successfully create the object itself and initialize its "waxy" attributes. But the second inherited constructorTopping::Dessert::new( )will never be invoked, so the "edible" attributes of the object will never be initialized. Likewise, in the Shimmer class's destructor, the nested call to $self->SUPER::DESTROY( ) will be dispatched to the left-most base class only. The Wax::Floor destructor will be called, but not the destructor for Topping::Dessert.

Curiously, the real problem here is not that there aren't enough constructor and destructor calls; it's that there are too many. The correct way to handle the problem is to ensure that there is only ever one call to new( ) during each construction and only one call to DESTROY( ) during each destruction. You then arrange for those single calls to correctly coordinate the initialization and cleanup for every class in the object's hierarchy.

To achieve that, individual classes can no longer be responsible for their own memory allocation or object blessing, nor for their own destruction; they must not have their own new( ) or DESTROY( ) methods. Instead, they should inherit a suitable new( ) and DESTROY( ) from some standard base class. And, as every class must automatically inherit the same constructor and destructor for this scheme to work correctly, it makes sense to put that constructor and destructor in the one class that every other class automatically inherits: UNIVERSAL. The necessary code is shown in Example 16-9.

Example 16-9. Implementing a universal constructor and destructor

package UNIVERSAL;
use List::MoreUtils qw( uniq );

# Return a list of the base classes of the class passed as an argument...
sub _hierarchy_of { my ($class, $reversed) = @_; no strict 'refs';
# ...needed to make the '::ISA' look-ups run silent

    # Start with the class, and its parents...
my @hierarchy = ( $class ); my @parents = $reversed ? reverse @{$class . '::ISA'} : @{$class . '::ISA'} ;
# For each parent, add it to the hierarchy and remember the grandparents...
while (defined (my $parent = shift @parents)) { push @hierarchy, $parent; push @parents, $reversed ? reverse @{$parent . '::ISA'} : @{$parent . '::ISA'} ; }
# Sort the (unique) classes most-basic first...
my @traversal_order = sort { $a->isa($b) ? -1 : $b->isa($a) ? +1 : 0 } uniq @hierarchy;
# Return in appropriate traversal order...
return reverse @traversal_order if $reversed; return @traversal_order; } use Memoize; memoize '_hierarchy_of'; use Class::Std::Utils;
# Universal constructor is shared by every class. It allocates their objects

# and coordinates their initializations...
sub new { my ($class, $arg_ref) = @_;

    # Create an inside-out object of the desired class...
my $new_obj = bless anon_scalar( ), $class; my $new_obj_ident = ident($new_obj);
# Iterate all base classes, visiting the most basic classes first...
for my $base_class (_hierarchy_of($class, 'reversed')) { no strict 'refs';
# ...needed for the '::BUILD' look-up

        # If this particular base class defines a BUILD( ) method...
if (my $build_ref = *{$base_class.'::BUILD'}{CODE}) {
# Extract the correct set of initializers...
my %arg_set = extract_initializers_from($arg_ref, {class => $base_class} );
# Then call the class's BUILD( ) method...
$build_ref->($new_obj, $new_obj_ident, \%arg_set); } } return $new_obj; } sub DESTROY { my ($self) = @_; my $ident = ident($self);
# Iterate all base classes, visiting the most derived classes first...
for my $base_class (_hierarchy_of(ref $self)) { no strict 'refs';
# ...needed for the '::DEMOLISH' look-up

        # If this particular base class defines a DEMOLISH( ) method...
if (my $demolish_ref = *{$base_class.'::DEMOLISH'}{CODE}) {
# Then call the class's DEMOLISH( ) method...
$demolish_ref->($self, $ident); } } return; }

The _hierarchy_of( ) subroutine traverses the inheritance tree from a given class upwards, and returns a list of the classes it inherits from. Normally, that list is sorted so that every derived class appears before any base class that it inherits from, unless the $reversed argument is true, in which case the list is sorted base-before-any-derived[*].

[*] Note that neither of those orderings are the same as the class traversal order that Perl's normal method dispatch uses. That is: self, first ancestor, second ancestor, third ancestor, etc. If two or more ancestral classes inherit from a common base class (a situation known as "diamond" inheritance), it's possible for the shared base class to be visited before the second of its derived classes is seen. That sequence would create problems in destructors, because the second derived class may be relying on base class components that no longer exist. Hence the more sophisticated sorting order employed in _hierarchy_of( ).

The UNIVERSAL::new( ) method starts out like any other constructor, creating an inside-out object in the usual way. Having created the object, new( ) then walks down through the class hierarchy from most basic to most derived (hence the 'reversed' flag on the call to _hierarchy_of( )). For each of these ancestral classes, it looks in the corresponding symbol table (*{$base_class.'::BUILD'}) to see whether that class has a BUILD( ) method defined (*{$base_class.'::BUILD'}{CODE}). If so, the universal constructor will call that method, passing it the appropriate initializer values (as recommended in the earlier "Constructor Arguments" guideline).

As a result, every class everywhere inherits the same constructor, which creates an inside-out object and then calls every BUILD( ) method it can find anywhere in the class's hierarchy. Each class-specific BUILD( ) is passed the object, followed by its unique identifier (to avoid recomputing that value in every class), and finally a hash containing the appropriate constructor arguments.

Similarly, the UNIVERSAL::DESTROY( ) method walks back up through the object's class hierarchy, most-derived classes first (so no 'reversed' flag). Using the same kind of symbol-table inspections as the constructor, it looks for and calls any DEMOLISH( ) method in any ancestral class, passing it the object and its unique identifying number.

All of which means that no class now has to define its own constructor or destructor. Instead, they can all just define an initializer (BUILD( )) and a clean-up method (DEMOLISH( )), which will be called in the appropriate sequence, taking into account multiple inheritance relationships of the class's inheritance hierarchy.

With this facility in place, you could rewrite the various wax and topping classes as shown in Example 16-10.

Example 16-10. Using the universal constructor and destructor

package Wax::Floor;
{
    
# Attributes...
my %name_of; my %patent_of; sub BUILD { my ($self, $ident, $arg_ref) = @_; $name_of{$ident} = $arg_ref->{name}; $patent_of{$ident} = $arg_ref->{patent}; return; } sub DEMOLISH { my ($self, $ident) = @_; delete $name_of{$ident}; delete $patent_of{$ident}; return; } } package Topping::Dessert; {
# Attributes...
my %name_of; my %flavour_of; sub BUILD { my ($self, $ident, $arg_ref) = @_; $name_of{$ident} = $arg_ref->{name}; $flavour_of{$ident} = $arg_ref->{flavour}; return; } sub DEMOLISH { my ($self, $ident) = @_; delete $name_of{$ident}; delete $flavour_of{$ident}; return; } }

Then the Shimmer class could (correctly!) inherit them both like so:


    package Shimmer;
    use base qw( Wax::Floor  Topping::Dessert );
    {
        
# Attributes...
my %name_of; my %patent_of; sub BUILD { my ($class, $ident, $arg_ref) = @_; $name_of{$ident} = $arg_ref->{name}; $patent_of{$ident} = $arg_ref->{patent}; return; } sub DEMOLISH { my ($self, $ident) = @_; delete $name_of{$ident}; delete $patent_of{$ident}; return; } }

Having factored out the common construction and destruction tasks, notice how much less work is now required to implement individual classes. More importantly, the code implementing derived classes is now totally decoupled from its base classes: no more ancestral constructor calls via $class->SUPER::new( ).

This approach to implementing classes is cleaner, more robust, far more scalable, and easier to maintain. It also ensures that every class is implemented in a consistent fashion, and can interoperate (under multiple inheritance) with any other class that is implemented using the same techniques.

Note that it's still possible for individual classes to provide their own constructors and destructors when that's desirable, so this technique allows legacy or non-standard classes to be used as wellthough not, of course, with the same guarantees of robustness.

    Previous
    Table of Contents
    Next
    © 2000- NIV