9 POLYMORPHISM AND DYNAMIC BINDING
Inheritance is not just a module combination and enrichment mechanism.
It also enables the definition of flexible entities that may become attached
to objects of various forms at run time, a property known as polymorphism.
This remarkable facility must be reconciled with static typing. The
language convention is simple: an assignment of the form a := b
is permitted not only if a and b are of the same type, but
more generally if a and b are of reference types A
and B, based on classes A and B such that B
is a descendant of A.
This corresponds to the intuitive idea that a value of a more specialized
type may be assigned to an entity of a less specialized type -- but not
the reverse. (As an analogy, consider that if you request vegetables, getting
green vegetables is fine, but if you ask for green vegetables, receiving
a dish labeled just "vegetables" is not acceptable, as it could
include, say, carrots.)
What makes this possibility particularly powerful is the complementary
facility: feature redefinition. A class may redefine some or all
of the features which it inherits from its parents. For an attribute or
function, the redefinition may affect the type, replacing the original
by a descendant; for a routine it may also affect the implementation, replacing
the original's routine body by a new one.
Assume for example a class POLYGON, describing polygons, whose
features include an array of points representing the vertices and a function
perimeter which computes a polygon's perimeter by summing the successive
distances between adjacent vertices. An heir of POLYGON may begin
as:
- class RECTANGLE inherit
- POLYGON
- redefine perimeter end
- feature --
Specific features of rectangles, such as:
- side1: REAL; side2: REAL
- perimeter: REAL is
- -- Rectangle-specific version
- do
- Result := 2 * (side1 + side2)
- end
- ... Other RECTANGLE features ...
Here it is appropriate to redefine perimeter for rectangles as
there is a simpler and more efficient algorithm. Note the explicit redefine
subclause (which would come after the rename if present).
Other descendants of POLYGON may also have their own redefinitions
of perimeter. The version to use in any call is determined by the
run-time form of the target. Consider the following class fragment:
- p: POLYGON; r: RECTANGLE
...
- create p; create r;
...
- if c then
- p := r
- end
- print (p.perimeter)
The polymorphic assignment p := r is valid because of
the above rule. If condition c is false, p will be attached to an
object of type POLYGON for the computation of p.perimeter,
which will thus use the polygon algorithm. In the opposite case, however,
p will be attached to a rectangle; then the computation will use
the version redefined for RECTANGLE. This is known as dynamic
binding.
Dynamic binding provides a high degree of flexibility. The advantage
for clients is the ability to request an operation (such as perimeter computation)
without explicitly selecting one of its variants; the choice only occurs
at run-time. This is essential in large systems, where many variants may
be available; each component must be protected against changes in other
components.
This technique is particularly attractive when compared to its closest
equivalent in traditional approaches. In Pascal or Ada, you would need
records with variant components, and case instructions to
discriminate between variants. This means that every client must know about
every possible case, and that any extension may invalidate a large body
of existing software.
These techniques support a development mode in which every module is
open and incremental. When you want to reuse an existing class but need
to adapt it to a new context, you can always define a new descendant of
that class (with new features, redefined ones, or both) without any change
to the original. This facility is of great importance in software development,
an activity which -- whether by design or by circumstance -- is invariably
incremental.
The power of polymorphism and dynamic binding demands adequate controls.
First, feature redefinition, as seen above, is explicit. Second, because
the language is typed, a compiler can check statically whether a feature
application a.f is correct. In contrast, dynamically typed object-oriented
languages defer checks until run-time and hope for the best: if an object
"sends a message" to another (that is to say, calls one of its
routines) one just expects that the corresponding class, or one of its
ancestors, will happen to include an appropriate routine; if not, a run-time
error will occur. Such errors will not happen during the execution of a
type-checked Eiffel system.
In other words, the language reconciles dynamic binding with
static typing. Dynamic binding guarantees that whenever more than
one version of a routine is applicable the right version (the one
most directly adapted to the target object) will be selected. Static typing
means that the compiler makes sure there is at least one such version.
This policy also yields an important performance benefit: in contrast
with the costly run-time searches that may be needed with dynamic typing
(since a requested routine may not be defined in the class of the target
object but inherited from a possibly remote ancestor), the EiffelBench
implementation always finds the appropriate routine in constant-bounded
time.
Assertions provide a further mechanism for controlling the power of
redefinition. In the absence of specific precautions, redefinition may
be dangerous: how can a client be sure that evaluation of p.perimeter
will not in some cases return, say, the area? Preconditions and postconditions
provide the answer by limiting the amount of freedom granted to eventual
redefiners. The rule is that any redefined version must satisfy a weaker
or equal precondition and ensure a stronger or equal postcondition than
in the original. In other words, it must stay within the semantic boundaries
set by the original assertions.
The rules on redefinition and assertions are part of the Design by Contract
theory, where redefinition and dynamic binding introduce subcontracting.
POLYGON, for example, subcontracts the implementation of perimeter
to RECTANGLE when applied to any entity that is attached at run-time
to a rectangle object. An honest subcontractor is bound to honor the contract
accepted by the prime contractor. This means that it may not impose stronger
requirements on the clients, but may accept more general requests, so that
the precondition may be weaker; and that it must achieve at least as much
as promised by the prime contractor, but may achieve more, so that the
postcondition may be stronger.
|