[ Team LiB ] Previous Section Next Section

10.4 Additional Instance Variables in Subclasses

One of the nice things about using a hash for a data structure is that derived classes can add additional instance variables without the superclass needing to know of their addition. For example, let's derive a RaceHorse class that is everything a Horse is but also tracks its win/place/show/lose standings. The first part of this is trivial:

{ package RaceHorse;
  our @ISA = qw(Horse);
  ...
}

You'll also want to initialize "no wins of no races" when you create the RaceHorse. You do this by extending the named subroutine and adding four additional fields (wins, places, shows, losses, for first-, second-, and third-place finishes, and none of the above):

{ package RaceHorse;
  our @ISA = qw(Horse);
  ## extend parent constructor:
  sub named {
    my $self = shift->SUPER::named(@_);
    $self->{$_} = 0 for qw(wins places shows losses);
    $self;
  }
}

Here, you pass all parameters to the superclass, which should return a fully formed Horse. However, because you pass RaceHorse as the class, it'd be already blessed into the RaceHorse class.[8] Next, add the four instance variables that go beyond those defined in the superclass, setting their initial values to 0. Finally, return the modified RaceHorse to the caller.

[8] Similar to the way the Animal constructor creates a Horse, not an Animal, when passed Horse as the class.

It's important to note here that we've actually "opened the box" a bit while writing this derived class. You know that the superclass uses a hash reference and that the superclass hierarchy doesn't use the four names chosen for a derived class. This is because RaceHorse will be a "friend" class (in C++ or Java terminology), accessing the instance variables directly. If the maintainer of Horse or Animal ever changes representation or names of variables, there could be a collision, which might go undetected except for that important day when you're showing off your code to the investors. Things get even more interesting if the hashref is changed to an arrayref as well.

One way to decouple this dependency is to use composition rather than inheritance as a way to create a derived class. In this example, you need to make a Horse object an instance variable of a RaceHorse and put the rest of the data in separate instance variables. You also need to pass any inherited method calls on the RaceHorse down to the Horse instance, through delegation. However, even though Perl can certainly support the needed operations, that approach is usually slower and more cumbersome. Enough on that for this discussion, however.

Next, let's provide some access methods:

{ package RaceHorse;
  our @ISA = qw(Horse);
  ## extend parent constructor:
  sub named {
    my $self = shift->SUPER::named(@_);
    $self->{$_} = 0 for qw(wins places shows losses);
    $self;
  }
  sub won { shift->{wins}++; }
  sub placed { shift->{places}++; }
  sub showed { shift->{shows}++; }
  sub lost { shift->{losses}++; }
  sub standings {
    my $self = shift;
    join ", ", map "$self->{$_} $_", qw(wins places shows losses);
  }
}

my $racer = RaceHorse->named("Billy Boy");
# record the outcomes: 3 wins, 1 show, 1 loss
$racer->won;
$racer->won;
$racer->won;
$racer->showed;
$racer->lost;
print $racer->name, " has standings of: ", $racer->standings, ".\n";

This prints:

Billy Boy has standings of: 3 wins, 0 places, 1 shows, 1 losses.
[Billy Boy has died.]
[Billy Boy has gone off to the glue factory.]

Note that we're still getting the Animal and Horse destructor. The superclasses are unaware that we've added four additional elements to the hash and so, still function as they always have.

    [ Team LiB ] Previous Section Next Section