Previous Page
Next Page

15.12. Indirect Objects

Don't use the indirect object syntax.

Quite simply: indirect object syntax is ambiguous. Whereas an "arrowed" method call is certain to call the corresponding method:


    my $female_parent = $family->mom( );
    my $male_parent   = $family->pop( );

with an indirect object call, the outcome is not at all certain:

    my $female_parent = mom $family;    # Sometimes the same as: $family->mom( )
    my $male_parent   = pop $family;    # Never the same as: $family->pop( )

The pop( ) case is fairly obvious: Perl assumes you're calling the built-in pop function...and then complains that it's not being applied to an array[*]. The potential problem in the mom( ) case is a little more subtle: if there's a mom( ) subroutine declared in the package in which mom $family is called, then Perl will interpret that call as mom($family) instead (that is, as a subroutine call, rather than as a method call).

[*] Even that helpful message can be confusing when you're working in a method-call mindset: "I thought methods could be called only on scalars? And why would the Family::pop( ) method require a polygamous array of families anyway?"

Unfortunately, that particular problem often bites under the most common use of the indirect object syntax: constructor calls. Many programmers who would otherwise never write indirect object method calls will happily call their constructors that way:

    my $uniq_id = new Unique::ID;

The problem is that they often do this kind of thing in the method of some other class. For example, they might decide to improve the Dogtag class by using Unique::ID objects as serial numbers:

    package Dogtag;
    use Class::Std::Utils;
    {
        # Attributes...
        my %name_of;
        my %rank_of;
        my %serial_num_of;

        
        # The usual inside-out constructor...
        sub new {
            my ($class, $arg_ref) = @_;

            my $new_object = bless anon_scalar( ), $class;

            # Now using special objects to ensure serial numbers are unique...
            $serial_num_of{ident $new_object} = new Unique::ID;

            return $new_object;
        }

That approach works fine, until they decide they need to factor it out into a separate class method:

        
        # The usual inside-out constructor...
        sub new {
            my ($class, $arg_ref) = @_;

            my $new_object = bless anon_scalar( ), $class;

            # Now allocating serial numbers polymorphically...
            $serial_num_of{ident $new_object} =  $class->_allocate_serial_num( );

            return $new_object;
        }

        
        # Override this method in any derived class that needs a
        
        # different serial number allocation mechanism...
        sub _allocate_serial_num {
            return new Unique::ID;
        }

As soon as they make this change, the first call to Dogtag->new( ) produces the exception:


    Can't locate object method "_allocate_serial_num" via package "Unique::ID"
    at Dogtag.pm line 17.

where line 17 is (mysteriously) the assignment:

            $serial_num_of{ident $new_object} =  $class->_allocate_serial_num( );

What happened? Previously, when the new Unique::ID call was still directly inside new( ), that call had to be compiled before new( ) itself could be completely defined. Thus, when the compiler looked at the call, there wasas yetno subroutine named new( ) defined in the current package, so Perl interpreted new Unique::ID as an indirect method call.

But once the new Unique::ID call has been factored out into a method that's defined after new( ), then the call will be compiled after the compilation of new( ) is complete. So, this time, when the compiler looks at that call, there is a subroutine named new( ) already defined in the current package. So Perl interprets new Unique::ID as a direct unparenthesized subroutine call (to the subroutine Dogtag::new( )) instead. Which means that it immediately calls DogTag::new( ) again, this time passing the string 'Unique::ID' as the sole argument. And when that recursive call to new( ) reaches line 17 again, $class will now contain the 'Unique::ID' string, so the $class->_allocate_serial_num( ) call will attempt to call the non-existent method Unique::ID::_allocate_serial_num( ), and the mysterious exception will be thrown.

That code is hard enough to debug, but it could also have gone wrong in a much more subtle and silent way. Suppose the Unique::ID class actually did happen to have its own _allocate_serial_num( ) method. In that case, the recursive call from Dogtag::_allocate_serial_num back into to the Dogtag constructor wouldn't fail; it would instead put whatever value was returned by the call to Unique::ID->_allocate_serial_num( ) into the $serial_num{ident $self} attribute of the object being created by the recursive Dogtag constructor call, and then return that object. Back in the original constructor call, that Dogtag object would then be assigned to yet another $serial_num{ident $self} attribute: this time the one for the object created in the non-recursive constructor call. The outermost constructor would also succeed and return its own Dogtag object.

But, now, instead of having a Unique::ID object for its serial number, that final Dogtag object would possess a serial number that consisted of a (nested) Dogtag object, whose own serial number attribute would contain whatever kind of value Unique::ID::_allocate_serial_num( ) happened to return: perhaps a Unique::ID object, or possibly a raw string, or maybe even just undef (if Unique::ID::_allocate_serial_num( ) happened to be a mutator method that merely updates its own object and doesn't return a value at all).

Pity the poor maintenance programmer who has to unravel that mess[*].

[*] If you got lost reading the explanation of this problem, you can no doubt imagine how hard it would be to debug the error in live code.

Indirect object method calls are ambiguous, brittle, fickle, and extremely context-sensitive. They can be broken simply by moving them about within a file, or by declaring an entirely unrelated subroutine somewhere else in the current package. They can lead to complex and subtle bugs. Don't use them.

    Previous Page
    Next Page