By
itself, the idea of an object is a convenient tool. It allows you to package
data and functionality together by
concept,
so you can represent an appropriate problem-space idea rather than being forced
to use the idioms of the underlying machine. These concepts are expressed as
fundamental units in the programming language by using the
class
keyword.
It
seems a pity, however, to go to all the trouble to create a class and then be
forced to create a brand new one that might have similar functionality.
It’s nicer if we can take the existing class, clone it and make additions
and modifications to the clone. This is effectively what you get with
inheritance,
with the exception that if the original class (called the
base
or
super
or
parent
class) is changed, the modified “clone” (called the
derived
or
inherited
or
sub
or
childclass)
also reflects those changes.
(The
arrow in the above UML diagram points from the derived class to the base class.
As you shall see, there can be more than one derived class.)
A
type does more than describe the constraints on a set of objects; it also has a
relationship with other types. Two types can have characteristics and behaviors
in common, but one type may contain more characteristics than another and may
also handle more messages (or handle them differently). Inheritance expresses
this similarity between types with the concept of base types and derived types.
A base type contains all the characteristics and behaviors that are shared
among the types derived from it. You create a base type to represent the core
of your ideas about some objects in your system. From the base type, you derive
other types to express the different ways that core can be realized.
For
example, a trash-recycling machine sorts pieces of trash. The base type is
“trash,” and each piece of trash has a weight, a value, and so on
and can be shredded, melted, or decomposed. From this, more specific types of
trash are derived that may have additional characteristics (a bottle has a
color) or behaviors (an aluminum can may be crushed, a steel can is magnetic).
In addition, some behaviors may be different (the value of paper depends on its
type and condition). Using inheritance, you can build a type hierarchy that
expresses the problem you’re trying to solve in terms of its types.
A
second example is the classic shape problem, perhaps used in a computer-aided
design system or game simulation. The base type is “shape,” and
each shape has a size, a color, a position, and so on. Each shape can be drawn,
erased, moved, colored, etc. From this, specific types of shapes are derived
(inherited): circle, square, triangle, and so on, each of which may have
additional characteristics and behaviors. Certain shapes can be flipped, for
example. Some behaviors may be different (calculating the area of a shape). The
type hierarchy embodies both the similarities and differences between the shapes.
Casting
the solution in the same terms as the problem is tremendously beneficial
because you don’t need a lot of intermediate models to get from a
description of the problem to a description of the solution. With objects, the
type hierarchy is the primary model, so you go directly from the description of
the system in the real world to the description of the system in code. Indeed,
one of the difficulties people have with object-oriented design is that
it’s too simple to get from the beginning to the end. A mind trained to
look for complex solutions is often stumped by this simplicity at first.
When
you inherit from an existing type, you create a new type. This new type
contains not only all the members of the existing type (although the
private
ones are hidden away and inaccessible), but more importantly it duplicates the
interface of the base class. That is, all the messages you can send to objects
of the base class you can also send to objects of the derived class. Since we
know the type of a class by the messages we can send to it, this means that the
derived class
is
the same type as the base class
.
In the above example, “a circle is a shape.” This type equivalence
via inheritance is one of the fundamental gateways in understanding the meaning
of object-oriented programming.
Since
both the base class and derived class have the same interface, there must be
some implementation to go along with that interface. That is, there must be
some code to execute when an object receives a particular message. If you
simply inherit a class and don’t do anything else, the methods from the
base-class interface come right along into the derived class. That means
objects of the derived class have not only the same type, they also have the
same behavior, which isn’t particularly interesting.
You
have two ways to differentiate your new derived class from the original base
class. The first is quite straightforward: you simply add brand new functions
to the derived class. These new functions are not part of the base class
interface. This means that the base class simply didn’t do as much as you
wanted it to, so you added more functions. This simple and primitive use for
inheritance is, at times, the perfect solution to your problem. However, you
should look closely for the possibility that your base class might also need
these additional functions. This process of discovery and iteration of your
design happens regularly in object-oriented programming.
Although
inheritance may sometimes imply that you are going to add new functions to the
interface, that’s not necessarily true. The second way to differentiate
your new class is to
change
the behavior of an existing base-class function. This is referred to as
overriding
that function.
To
override a function, you simply create a new definition for the function in the
derived class. You’re saying “I’m using the same interface
function here, but I want it to do something different for my new type.”
Is-a
vs. is-like-a relationships
There’s
a certain debate that can occur about inheritance: Should inheritance override
only
base-class functions (and not add new member functions that aren’t in the
base class)? This would mean that the derived type is
exactly
the same type as the base class since it has exactly the same interface. As a
result, you can exactly substitute an object of the derived class for an object
of the base class. This can be thought of as
pure
substitution
,
and it’s often referred to as the
substitution
principle
.
In a sense, this is the ideal way to treat inheritance. We often refer to the
relationship between the base class and derived classes in this case as an
is-a
relationship, because you can say “a circle
is
a
shape.” A test for inheritance is whether you can state the is-a
relationship about the classes and have it make sense.
There
are times when you must add new interface elements to a derived type, thus
extending the interface and creating a new type. The new type can still be
substituted for the base type, but the substitution isn’t perfect because
your new functions are not accessible from the base type. This can be described
as an
is-like-a
relationship; the new type has the interface of the old type but it also
contains other functions, so you can’t really say it’s exactly the
same. For example, consider an air conditioner. Suppose your house is wired
with all the controls for cooling; that is, it has an interface that allows you
to control cooling. Imagine that the air conditioner breaks down and you
replace it with a heat pump, which can both heat and cool. The heat pump
is-like-an
air conditioner, but it can do more. Because the control system of your house
is designed only to control cooling, it is restricted to communication with the
cooling part of the new object. The interface of the new object has been
extended, and the existing system doesn’t know about anything except the
original interface.
Of
course, once you see this design it becomes clear that the base class
“cooling system” is not general enough, and should be renamed to
“temperature control system” so that it can also include heating
– at which point the substitution principle will work. However, the above
diagram is an example of what happens in design and in the real world.
When
you see the substitution principle it’s easy to feel like this approach
(pure substitution) is the only way to do things, and in fact it
is
nice if your design works out that way. But you’ll find that there are
times when it’s equally clear that you must add new functions to the
interface of a derived class. With inspection both cases should be reasonably
obvious.