When you're building an extension to your house, or customizing a car, or upgrading your computer, you normally start with an existing blueprint and add on (or replace) certain bits. If your original blueprints are good, it's a waste of time and resources to start from nothing and separately reconstruct the bits you already have.
The same thing happens in object-oriented programming. Sometimes you already have a class of objects that partially meets your requirements (say a class that represents a truck), and you just want to tweak that class, to create one that exactly meets your needs (say a class that represents a fire-truck).
In an object-oriented language, it's not necessary to code that new class from scratch, reproducing (or maybe cutting and pasting) the original truck code, and then adding new methods to implement alarms, ladders, hoses, red braces, etc.
Instead, you can just tell the program that the new FireTruck class is based on (or inherits) the existing Truck class. Then you add certain extra features (i.e. additional data and methods) to the FireTruck class, over and above those it inherited from the Truck class. That's the Laziest possible solution, since you only have to specify the new stuff you want and then say "and do the rest the same way that class over there does it".
Any class like FireTruck that inherits attributes and methods from another class is called a derived class or sometimes a child class. The class from which it inherits (i.e. Truck in this case) is called its base class or its parent class. The relationship between a base class and its derived class is called the is-a relationship, because an object of a derived class must necessarily have all the attributes and methods of an object of the base class, so it "is a" base-class object for all practical purposes. This idea corresponds well with many relationships in the Real World: a fire-truck is-a truck, an automated teller machine is-a machine, a hench-person is-a person, an unnecessarily long list of analogies is-a list of analogies.
The is-a relationship is itself inherited (or perhaps, "meta-inherited"), so you can have increasingly general categories over more than two levels: a fire-truck is-a truck is-a vehicle is-a device is-a thing; a hench-person is-a person is-a animal is-a life-form is-a thing. If you trace most classes back to their most ancestral categories, they all end up being a "thing".
Note however that is-a
isn't a two-way street. Though an object of a derived class can always
be treated like an object of a base class, it's not true that an object
of a base class necessarily is-a object of a derived class. That
is, although a fire-truck always is-a truck, it's not the case that
a truck always is-a fire-truck.
Figure 1 -- Inheriting from the Truck class
Using inheritance means that you only have to specify how a fire-truck or a cement mixer or an armoured car differs from a regular truck, rather than constantly needing to restate all the standard ways in which it's the same as a truck. That makes the code that defines each type of truck shorter, which means there's far less work to create it.
More importantly, it reduces your maintenance load too, because any change to the behaviour of the general Truck class (for example, modifying its register() method in response to some change in transport regulations) is automatically propagated to all the specific truck classes (FireTruck, DumpTruck, ArmouredCar, etc.) that inherit from Truck.
In this way, inheritance
also provides a means of capturing the abstract relationships between
specific classes of object within a program. The class of fire-trucks is
a special case of the more general class of trucks, which in turn might
be a special case of the very general class of vehicles. The more abstract
classes are generalized blueprints that define the common features of a
wide range of kinds of objects (such as the standard regulations covering
all vehicles). The more specialized classes presuppose those common features,
and then describe the additional attributes and methods unique to their
particular kind of object (such as the special regulations covering fire-trucks).
package PerlGuru; @ISA = ( "PerlHacker" );And that's it! From that point on, whenever Perl needs to determine if PerlGuru has any inherited methods, it checks the contents of the array @PerlGuru::ISA. Any package whose name appears in that array is considered to be a parent class of PerlGuru. Of course, since it's an array, we can have many class names in @PerlGuru::ISA, allowing the class to inherit methods from more than one parent (a phenomenon known somewhat unimaginatively as multiple inheritance):
package PerlGuru; @ISA = qw( PerlHacker LanguageMaestro Educator PunMeister );And, of course, if those four parent classes also inherited from other classes:
package PerlHacker; @ISA = qw( Programmer Obfuscator );
package PunMeister; @ISA = qw( Writer Humorist OneSickPuppy );then PerlGuru would also inherit methods from those "grandparents". All this inheritance creates the hierarchy shown in Figure 2.
Figure 2 -- PerlGuru's inheritance hierarchy
In other words, if you call:
my $guru = PerlGuru->new();
# and later...
my $question = <>; print $guru->answer($question);then, if class PerlGuru doesn't provide a PerlGuru::answer method, Perl starts trying the various parent classes (as specified by the current value of the @PerlGuru::ISA array).
The parents are searched in a depth-first recursive sequence, so Perl would look for one of the following (in this order):
Note that, when looking in a parent class, Perl checks the left-most parent first, and then checks the left-most parent of that class, and the left-most parent of that class etc. Hence, if a class's left-most great-great-great-grandparent has an answer method then that method will be called, even if another of the object's direct parents also has a suitable method.
If you're used to the complicated inheritance semantics in some other object-oriented language, it's important to realize that inheritance in Perl is merely a way of specifying where else to look for a method, and nothing else! There is no direct inheritance of attributes (unless you arrange for it), nor any hierarchical calling of constructors or destructors (unless you explicitly write those methods that way), nor any compile-time consistency checks of the interface or implementation of derived classes.
Inheritance just tells
you were to look next for a missing method.
But that's actually back-to-front, because the way in which a particular person will respond to your message depends on the kind of person they already are. A ReceptivePerson will respond enthusiastically, a ShyPerson will respond tentatively, and a JustPlainWeirdPerson will probably respond in iambic pentameter. The original message is always the same; the response depends on the kind of person who receives it.
Language theorists2 call this type of behaviour polymorphism. When a method is called on a particular object, the actual method that's involved depends on the class to which the object belongs. For instance, if you call an object's ignite() method, its response will be quite different depending on whether it belongs to the Paper, Rocket, Passion, or FlameWar class.
Randomly calling an identically
named method on objects of different classes is not, of course, a recommended
programming technique. However, polymorphic behaviour does prove extremely
useful when there is some explicit relationship between the various classes
of objects, or when there is an implicit relationship or a common universal
property shared between them. The following subsections discuss each of
these cases.
Typically, the objects representing the various trucks would be collected in some kind of container, probably a list. Some operations will need to be carried out on individual objects (e.g. register this particular truck, schedule an inspection for that one, etc.), but many tasks will have to be performed on every truck in the system (e.g. send out an annual registration notice for each, print a complete list of recent inspection dates, etc.)
For operations that need to be performed on every truck, the application is likely to walk along the list using a loop, calling the appropriate method for each object in turn. For example, the loop might call each object's print_registration_reminder() method.
The problem is that the actual procedure to be followed by each object may be different, depending on the actual kind of truck the object represents (i.e. the actual class to which it belongs). For instance, every truck needs to be registered, but the form for registering a semi-trailer may be very different from the one for a fire-truck, or for an armoured car. If that's the case, the processing loop will have to determine the class of each object and then find the corresponding method call for each distinct class. That's a pain to code, and a bigger pain to re-code every time you add or remove another class of truck.
This situation is the ideal place to use polymorphism. If the ancestral Truck class has a register() method, then you're guaranteed that every derived class also has a register() method (namely, the one that it inherits from Truck). However, when you specify the various derived classes, you may choose to replace the inherited register() method with one specific to the needs of the derived class.
Having given each class its own unique register() method, you can then walk the list of objects and simply call register() on each. You can be confident that each object can respond to that method call because at the very least they'll use the register() they inherited from the Truck class. However, if they have a more specialized way of registering themselves, then that more specialized method will be automatically invoked instead. In other words, you can arrange that each object has a register() method, but not necessarily the same register() method.
The result is that, although the loop code is very simple:
for each object in the list... call its register() methodthe response to those calls is always appropriate to the particular object on which the method is called.
Better yet, if you subsequently add a new class derived from Truck, and then put objects of that new class in the list, the old code will continue to work without modification. When the loop encounters an object of the new class, it will simply call that object's new register() method, and execute the new behaviour specified by the object's class definition. If the new class didn't define any new behaviour, the old behaviour inherited from class Truck will be used instead.
This kind of polymorphism is known as inheritance polymorphism, because the objects whose methods are called belong to a hierarchy of classes that are related by inheritance. The presence of the required method in the base class of the hierarchy ensures that objects of any derived class can always respond to a given method call. The ability to redefine individual methods in derived classes allows objects of those classes to respond more specifically to a particular method call, if they so wish.
All object-oriented languages
support inheritance polymorphism (it's part of the definition of "object
oriented"). For some languages, it's the only form of polymorphism they
permit. But it certainly isn't the only form that's possible. In fact,
there's no need for objects that are treated polymorphically to have any
kind of class relationship at all.
For example, since there are probably no actual Truck objects used in the truck registry application, there's no real need for the Truck class at all (at least, as far as the polymorphism in the registration loop is concerned). So long as each object in the list belongs to a class that has a register() method, the loop doesn't really care what their ancestral class was (i.e. whether they are trucks, truckers, trucking companies, or truculents). Provided they can each respond to a call on their register() method, the loop proceeds with serene indifference.
Of course, that's a mighty big proviso. With inheritance polymorphism you could be sure that every object in the list did have a register() method (at the very least, the one it inherited from Truck). With interface polymorphism there's no such guarantee.
Inheritance polymorphism is a special case of interface polymorphism, because a common base class guarantees that objects share a specific inherited method. Hence any language that supports interface polymorphism automatically supports inheritance polymorphism as well.
Perl is such a language.
In Perl, every method of every class is (potentially) polymorphic, as a direct consequence of the way that methods are automatically dispatched up the class hierarchy (as described above). There's no special syntax, no requirement for type-compatibility of method arguments, no need for inheritance relationships between classes. Just define your method, redefine it in any derived classes that need to act differently, and without even knowing it you're polymorphizing.
Suppose we have an object reference (say, $datum) and we call a method (say, print_me) on it:
foreach my $datum ( @data )
{
$datum->print_me();
}
The method dispatch mechanism
determines the class of the invoking object (i.e. of $datum),
and then looks in the corresponding package for a method of the appropriate
name (i.e. print_me).
Provided the object belongs to a class with a method named print_me,
the method call succeeds and some action is taken. That action depends
on the class of the invoking object, even though the call syntax is always
the same.
The elements in the @data array might have been blessed into completely unrelated classes:
my @data = (
GIF_Image->new(file=>"camelopard.gif", format=>"interlaced"),
XML::File->new("./lamasery.xml"),
PGP_Coded->new("Software is *not* a munition!"),
HTTP::Get->new("http://www.perl.org/news.html"),
Signature->new(),
);
but the same method
call (i.e. $datum->print_me())
handles them all appropriately, so long as each object's class's interface
provides a print_me
method. So Perl provides full interface polymorphism.
Of course, the dispatch mechanism also has a fall-back strategy if the class of the invoking object doesn't provide a matching method. As explained earlier, it immediately searches through the object's ancestor classes, trying to find an inherited method with the correct name.
This means that if the object belongs to a class that inherits a method named print_me, the method call succeeds and some ancestrally-defined action is taken. Once again, that action depends on the class of the particular object (or more accurately, on the "genealogy" of its class), even though the call syntax is still always the same. So Perl also provides full inheritance polymorphism.
As is usually the case
in Perl, the choice is yours.
[This article is adapted
from Damian Conway's new book "Object
Oriented Perl"]
2 ...most of whom live at ground-zero
in the JustPlainWeirdPerson category...