Приглашаем посетить
Татищев (tatischev.lit-info.ru)

Section 15.7.  Cloning

Previous
Table of Contents
Next

15.7. Cloning

Don't let a constructor clone objects.

If you overload your constructors to also clone objects, it's too hard to tell the difference between construction and copying in client code:

    $next_obj = $requested->new(\%args);     # New object or copy?

Methods that create new objects and methods that clone existing objects have a large amount of overlap in their behaviour. They both have to create a new data structure, bless it into an object, locate and verify the data to initialize its attributes, initialize its attributes, and finally return the new object. The only significant difference between construction and cloning is where the attribute data originates: externally in the case of a constructor, and internally in the case of a clone method.

The natural temptation is to combine the two methods into a single method. And the usual mental leap at that point is that Perl methods can always be called either as class methods or as instance methods. So, hey, why not simply have new( ) act like a constructor if it's called as a class method:

    $new_queue = Queue::Priority->new({ selector => \&most_urgent });

and then act like a cloning method if it's called on an existing object:

    $new_queue = $curr_queue->new( );

Because that can be achieved by adding only a single "paragraph" at the start of the existing constructor, as Example 15-3 illustrates. Cool!

Example 15-3. A constructor that also clones
sub new {
    my ($invocant, $arg_ref) = @_;

    # If method called on an object (i.e., a blessed reference)...
    if (ref $invocant) {
        # ...then build the argument list by copying the data from the object...
        $arg_ref = {
            selector => $selector_of{ident $invocant},
            data     => [ @{$data_of{ident $invocant} } ],
        }
    }

    # Work out the actual class name...
    my $class = ref($invocant)||$invocant;

    # Build the object...
    my $new_object = bless anon_scalar( ), $class;

    # And initialize its attributes...
    $selector_of{ident $new_object} = $arg_ref->{selector};
    $data_of{ident $new_object}     = $arg_ref->{data};

    return $new_object;
}

A variation on this idea is to allow constructor calls on objects, but have them still act like ordinary constructors, creating a new object of the same class as the object on which they're called:

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

        # Work out the actual class name...
        my $class = ref($invocant)||$invocant;

        # Build the object...
        my $new_object = bless anon_scalar( ), $class;

        # And initialize its attributes...
        $selector_of{ident $new_object} = $arg_ref->{selector};
        $data_of{ident $new_object}     = $arg_ref->{data};

        return $new_object;
    }

Unfortunately, there are several flaws in either of these approaches. The most obvious is that it suddenly becomes impossible to be sure what a given call to new( ) is actually doing. That is, there's no way to tell whether a statement like:

    $next_possibility->new( \%defaults );

is creating a new object or copying an existing one. At least, no way to tell without first determining what's in $next_possibility. If, for example, the call to new( ) is part of a processing loop like:

    
    # Investigate alternative storage mechanisms...
    for my $next_possibility ( @possible_container_classes ) {
        push @active_queues, $next_possibility->new( \%defaults );
        # etc.
    }

then it's (probably) a constructor, but if it's part of a loop like:

    
    # Examine possible data sources...
    for my $next_possibility ( @active_queues ) {
        push @phantom_queues, $next_possibility->new( \%defaults );
        # etc.
    }

then it's likely to be cloning. The point is, you can no longer tell what's happening just by looking at the code where it's happening. You can't even really tell by looking at the array that's being iterated, until you trace back further and work out what kind of values that array is actually storing.

In contrast, if new( ) only ever constructs, and cloning is always done with a method called clone( ), then the very same method call:


    $next_possibility->new( \%defaults );

is now clearly and unambiguously a constructor, regardless of context. Had it been intended to be a cloning operation, it wouldequally unambiguouslyhave been written:


    $next_possibility->clone( \%defaults );

Apart from not being able to say precisely what you mean, multipurpose constructors create a second maintenance problem: as Example 15-3 illustrates, adding cloning support needlessly complicates the constructor code itself. Especially when a separate clone( ) method can often be implemented far more cleanly in fewer lines of code, without modifying new( ) at all:


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

        
# Work out the object's class (and verify that it actually has one)...
my $class = ref $self or croak( qq{Can't clone non-object: $self} );

        # Construct a new object,

        # copying the current object's state into the constructor's argument list...
return $class->new({ selector => $selector_of{ident $self}, data => [ @{ $data_of{ident $self} } ], }); }

Separating your new( ) and clone( ) methods makes it possible to accurately encode your intentions in any code that creates new objects. That, in turn, makes understanding and debugging that code very much easier. Separate creation methods also make your class's own code cleaner and more maintainable.

Note that the same reasoning and advice applies in any situation where you're tempted to overload the behaviour of a single method or subroutine to provide two or more related functions. Resist that urge.

    Previous
    Table of Contents
    Next