Copyright Notice
This text is copyright by CMP Media, LLC, and is used with their permission. Further distribution or use is not permitted.This text has appeared in an edited form in SysAdmin/PerformanceComputing/UnixReview magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.
Please read all the information in the table of contents before using this article.
Unix Review Column 14 (May 1997)
In my previous column, I started examining Perl's Object-Oriented mechanisms. As an illustration, I created a ``class'' called ``Car'', and gave ``methods'' to access this class to allow passengers to enter, leave, and be accounted for. Although you can probably read this month's column without having the previous column in hand, I'd strongly recommend reading the previous one first. (If you have access to the Web, you can view the previous column online at http://www.stonehenge.com/merlyn/UnixReview/.)
So, continuing on with where I left off last time, let's take a couple of vehicles:
my $myCar = new Car; my $myTruck = new Truck;
Now, this should create a new Car and a new Truck. Now, these two vehicles will be similar in many respects, but different in others. We'll represent this using ``inheritance'' from a common ``Vehicle'' class, like so:
BEGIN { @Car::ISA = "Vehicle"; @Truck::ISA = "Vehicle"; }
The reason I'm wrapping this in a BEGIN
block is that it needs to
be executed before I call new
, even though I have written these
assignments later in the program. Typically, all @ISA settings should
happen before the first ``real'' line of code gets executed.
But what do these do? As you'll see in a moment, there is no
Car::new
method. Instead, Perl will look for such a method in the
Vehicle class. Similarly, there's no Truck::new
method. Instead,
all we have is the &Vehicle::new method:
sub Vehicle::new { bless my $self = {}, shift; my %parms = @_; $self->{Passengers} = $parms{Passengers} || {}; $self; }
Note that this is very similar to the Car::new
method from my last
column. The first line creates the empty anonymous hash, sticks it
into $self, and blesses it into the right package. Note that the
package comes from the first parameter, here grabbed with shift
.
The second line creates a named hash %parms from the remaining method
arguments.
The third line here is unlike Car::new
. I'm permitting the invoker
to pass in a list of passengers that will be initially loaded into
the car, like so:
$myCar = new Car Passengers => qw(Fred Dino);
If such a list is not specified, the car starts out empty. The final line returns the newly created car.
So, this method can be used from both the Car and Truck classes to create Cars and Trucks. So far, however, the only difference between a Car and a Truck is its class, so we seem to have gone to a lot of trouble for nothing. Let's add a little more behavior to these vehicles, and then I'll start differentiating them.
First, let's add back in the enter
, leave
, and jump
methods
from the previous column:
$myCar->enter(qw(Fred Barney)); $myTruck->enter(qw(Wilma Betty)); $myTruck->jump(To => $myCar, People => qw(Wilma)); $myCar->leave(qw(Fred Barney Wilma));
In human terms, Fred and Barney get in the car, while Wilma and Betty hop in the truck. Suddenly, Wilma gets out of the truck and into the car, then persuades Fred and Barney to also get out of the car with her. (Betty is still alone in the truck.) Let's see how that would look as the implementation:
sub Vehicle::enter { my $self = shift; for (@_) { $self->{Passengers}->{$_} = "seat"; } $self; }
Here, the object is saved away into $self, then for each of the
remaining method arguments, we add an entry into the Passengers
hash with the key for the name and ``seat'' for the value (as in the
previous column). Note that the method is defined in the Vehicle
class, making it accessible directly to both Cars and Trucks.
Similarly, we have the leave
method:
sub Vehicle::leave { my $self = shift; for (@_) { delete $self->{Passengers}->{$_}; } $self; }
And here, the only difference between enter
and <leave> is what
happens to the Passengers
hash. The jump
method is a little
scarier, but still pretty easy to read:
sub Vehicle::jump { my $self = shift; my %parms = @_; my $dest_vehicle = $parms{To} || new {ref $self}; my $people = $parms{People} || [keys %{$self->{Passengers}}]; $people = [$people] unless ref $people; $self->leave(@$people); $dest_vehicle->enter(@$people); $dest_vehicle; }
First, the object is saved into $self, and the remaining method
arguments form the hash %parms. A target vehicle has to be selected.
If there's a hash entry for To
, that's preferred, but if not, a new
vehicle is created.
But what type should the vehicle be? Well, a bit of arbitrary
decision making combined with years of programming experience led me
to conclude that the new unspecified vehicle should be of the same
size and shape as the vehicle being exited. We can get this by
calling ref $self
, which is then passed in as the ``object'' on a
new
call. (Wouldn't it be nice to be able to create a duplicate
object this easily in real life? But I digress...)
Next comes the designated jumpers. That's either gonna be the
People
parameter, or everyone currently on board. Note that
$people here is expected to be a listref, but if someone passes in a
scalar name, we'll have trouble later, so the line that begins
$people =
turns a simple name into a one-element anon-list if
necessary.
Once the source vehicle ($self), the people (@$people), and the
destination vehicle ($dest_vehicle) have been determined, we can start
moving people around. First, the people are asked to leave the source
vehicle, and then they are asked to join the destination vehicle.
Finally, the new vehicle is returned (which could be the same as the
one passed in as To
, or it might be a brand new Car or Truck).
Well, this will certainly implement everything necessary to handle that sequence of events above. To check our work, I'll need some way to access the current passenger list:
sub Vehicle::passengers { my $self = shift; sort keys %{self->{Passengers}}; }
Why don't I just let the users of the Car and Truck class know that $self->{Passengers} is a hash with the keys being the current passengers? Well, I could choose to do that, if I'm lazy, or performance is very critical. However, I would then be locked into that interface forever, even if I found out later that it would be easier to implement the passengers as a list instead of a hash. Hence, it's better to provide access methods such as this one, and encourage users not to ``peer behind the scenes''.
These vehicle classes have woefully little error checking. Let's add a bit of that, and illustrate why Car and Truck are different classes at the same time:
sub Vehicle::enter { my $self = shift; if (@_ + keys %{$self->{Passengers}} > $self->seats) { die "Cannot add @_ to $self"; } for (@_) { $self->{Passengers}->{$_} = "seat"; } $self; }
OK, let's see what's going on here... I'm adding the length of the
argument list (in @_) to the number of keys, to see if it's greater
than some number, returned by calling the seats
method. If it's
greater, then we've got too many people trying to sit in the car, and
death occurs.
But what is this seats
method? Well, I haven't defined it yet, but
it'll look like this:
sub Vehicle::seats { 2 }
Hmm. That wasn't hard. Now trucks and cars all have 2 seats, along with anything else that inherits from Vehicle. But wait -- cars can seat 4 people! So let's add that fact:
sub Car::seats { 4 }
When $myCar->seats gets called, it returns 4 (because we find seats
directly in Car
). But when $myTruck->seats gets called, there is
no Truck::seats
method, so Perl searches the inheritance path to
find the default seat-count in Vehicle
! For grins, we could define
a few more types:
BEGIN { @Motorcycle::ISA = "Vehicle"; @Van::ISA = "Vehicle"; @Unicycle::ISA = "Vehicle"; } sub Van::seats { 8 } sub Unicycle::seats { 1 }
And note that I didn't define a seats
for a Motorcycle, because the
default (2) was already correct.
What you're seeing here is an example of inheritance and method
overloading. Inheritance, in that seats
for 2-seaters comes
directly from Vehicle. Overloading, in that seats
for the others
overshadows the definition in Vehicle.
I hope you've enjoyed this brief stroll into Objectland, and can see some possibilities. Enjoy!