INH_MF75.PDF

(504 KB) Pobierz
16
Inheritance techniques
F
rom the last two chapters we have learned to appreciate inheritance as a key ingredient
in the object-oriented approach to reusability and extendibility. To complete its study we
must explore a few more facilities — something of a mixed bag, but all showing striking
consequences of the beauty of the basic ideas:
• How the inheritance mechanism relates to assertions and Design by Contract.
• The global inheritance structure, where all classes fit.
• Frozen features: when the Open-Closed principle does not apply.
• Constrained genericity: how to put requirements on generic parameters.
• Assignment attempt: how to force a type — safely.
• When and how to change type properties in a redeclaration.
• The mechanism of anchored declaration, avoiding redeclaration avalanche.
• The tumultuous relationship between inheritance and information hiding.
Two later chapters will pursue inheritance-related topics: the review of
typing
issues
in chapter
17,
and a detailed methodological discussion of
how to use inheritance
(and
how not to misuse it) in chapter
24.
Most of the following sections proceed in the same way: examining a consequence
of the inheritance ideas of the last two chapters; discovering that it raises a challenge or an
apparent dilemma; analyzing the problem in more depth; and deducing the solution. The
key step is usually the next-to-last one: by taking the time to pose the problem carefully,
we will often be led directly to the answer.
16.1 INHERITANCE AND ASSERTIONS
Because of its very power, inheritance could be dangerous. Were it not for the assertion
mechanism, class developers could use redeclaration and dynamic binding to change the
semantics of operations treacherously, without much possibility of client control. But
assertions will do more: they will give us deeper insights into the nature of inheritance. It
is in fact not an exaggeration to state that only through the principles of Design by
Contract can one finally understand what inheritance is really about.
570
IN HERITANCE TECHNIQUES §16.1
The basic rules governing the rapport between inheritance and assertions have already
been sketched: in a descendant class, all ancestors’ assertions (routine preconditions and
postconditions, class invariants) still apply. This section gives the rules more precisely and
uses the results obtained to take a new look at inheritance, viewed as subcontracting.
Invariants
We already encountered the rule for class invariants:
Parents’ Invariant rule
The invariants of all the parents of a class apply to the class itself.
The parents’ invariants are added to the class’s own, “addition” being here a logical
and then.
(If no invariant is given in a class, it is considered to have
True
as invariant.)
By induction the invariants of all ancestors, direct or indirect, apply.
As a consequence, you should not repeat the parents’ invariant clauses in the
invariant of a class (although such redundancy would be semantically harmless since
a
and then
a
is the same thing as
a).
The flat and flat-short forms of the class will show the complete reconstructed
See
“FLATTENING
THE STRUCTURE”,
invariant, all ancestors’ clauses concatenated.
15.3, page 541.
Preconditions and postconditions in the presence of dynamic binding
The case of routine preconditions and postconditions is slightly more delicate. The general
idea, as noted, is that any redeclaration must satisfy the assertions on the original routine.
This is particularly important if that routine was deferred: without such a constraint on
possible effectings, attaching a precondition and a postcondition to a deferred routine
would be useless or, worse, misleading. But the need is just as bad with redefinitions of
effective routines.
The exact rule will follow directly from a careful analysis of the consequences of
redeclaration, polymorphism and dynamic binding. Let us construct a typical case and
deduce the rule from that analysis.
Consider a class and one of its routines with a precondition and a postcondition:
r
is
require
α
ensure
β
end
C
A
The routine,
the client and
the contract
The figure also shows a client
C
of
A.
The typical way for
C
to be a client is to
include, in one of its routines, a declaration and call of the form
§16.1 IN HERITAN CE AND ASSERTIONS
571
a1: A
a1 r
q
For simplicity, we ignore any arguments that
r
may require, and we assume that
r
is
a procedure, although the discussion applies to a function just as well.
Of course the call will only be correct if it satisfies the precondition. One way for
C
to make sure that it observes its part of the contract is to protect the call by a precondition
test, writing it (instead of just
a1 r)
as
q
if
a1
α
then
a1 r
-- i.e. the postcondition holds
check
a1
β
end
Instructions that may assume
a1
β …
end
q
q
q
q
(As noted in the discussion of assertions, this is not required: it suffices to guarantee, with
or without an
if
instruction, that
α
holds before the call. We will assume the
if
form for
simplicity, and ignore any
else
clause.)
Having guaranteed the precondition, the client
C
is entitled to the postcondition on
return: after the call, it may expect that
a1
β
will hold.
q
All this is the basics of Design by Contract: the client
must
ensure the precondition
on calling the routine and, as a recompense,
may
count on the postcondition being satisfied
when the routine exits.
What happens when inheritance enters the picture?
r
is
require
α
ensure
β
end
r
++
is
require
γ
ensure
δ
end
The routine,
the client, the
contract and
the descendant
C
A
A'
Assume that a new class
A'
inherits from
A
and redeclares
r.
How, if at all, can it
change the precondition
α
into a new one
γ
and the postcondition
β
into a new one
δ?
To decide the answer, consider the plight of the client. In the call
a1 r
the target
a1
may now, out of polymorphism, be of type
A'
rather than just
A.
But
C
does not know
about this! The only declaration for
a1
may still be the original one:
q
a1: A
572
IN HERITANCE TECHNIQUES §16.1
which names
A,
not
A'.
In fact
C
may well use
A'
without its author ever knowing about
the existence of such a class; the call to
r
may for example be in a routine of
C
of the form
some_routine_of_C
(a1:
A)
is
do
…;
a1 r;
end
q
Then a call to
some_routine_of_C
from another class may use an actual argument of
type
A',
even though the text of
C
contains no mention of class
A'.
Dynamic binding means
that the call to
r
will in that case use the redefined
A'
version.
So we can have a situation where
C
is only a client of
A
but in fact will at run time
use the
A'
version of some features. (We could say that
C
is a “dynamic client” of
A'
even
though its text does not show it.)
What does this mean for
C?
The answer, unless we do something, is: trouble.
C
can
be an honest client, observing its part of the deal, and still be cheated on the result. In
if
a1
α
then
a1 r
end
q
q
if
a1
is polymorphically attached to an object of type
A',
the instruction calls a routine that
expects
γ
and guarantees
δ,
whereas the client has been told to satisfy
α
and expect
β.
So
we have a potential discrepancy between the client’s and supplier’s views of the contract.
How to cheat clients
To understand how to satisfy the clients’ expectations, we have to play devil’s advocate
and imagine for a second how we could fool them. It is all for a good cause, of course (as
with a crime unit that tries to emulate criminals’ thinking the better to fight it, or a
computer security expert who studies the techniques of computer intruders).
If we, the supplier, wanted to cheat our poor, honest
C
client, who guarantees
α
and
expects
β,
how would we proceed? There are actually two ways to evil:
• We could
require more
than the original precondition
α.
With a stronger
precondition, we allow ourselves to exclude (that is to say, not to guarantee any
specific result) for cases that, according to the original specification, were perfectly
acceptable.
Remember the point emphasized repeatedly in the discussion of Design by
Contract: making a precondition stronger facilitates the task of the supplier
(“the client is more often wrong”), as illustrated by the extreme case of
precondition
false
(“the client is always wrong”).
• We could
ensure less
than the original postcondition
β.
With a weaker postcondition,
we allow ourselves to produce less than what the original specification promised.
As we saw, an assertion is said to be stronger than another if it logically implies it,
and is different; for example,
x
>=
5
is stronger than
x
>=
0.
If
A
is stronger than
B, B
is
said to be weaker than
A.
§16.1 IN HERITAN CE AND ASSERTIONS
573
How to be honest
From understanding how to cheat we deduce how to be honest. When redeclaring a
routine, we may keep the original assertions, but we may also:
• Replace the precondition by a
weaker
one.
• Replace the postcondition by a
stronger
one.
The first case means being more generous than the original — accepting more cases.
This can cause no harm to a client that satisfies the original precondition before the call.
The second case means producing more than what was promised; this can cause no harm
to a client call that relies on the original postcondition being satisfied after the call.
Hence the basic rule:
Assertion Redeclaration rule (1)
A routine redeclaration may only replace the original precondition by one
equal or weaker, and the original postcondition by one equal or stronger.
The rule expresses that the new version must accept all calls that were acceptable to
the original, and must guarantee at least as much as was guaranteed by the original. It may
— but does not have to — accept more cases, or provide stronger guarantees.
As its name indicates, this rule applies to both forms of redeclaration: redefinitions
and effectings. The second case is particularly important, since it allows you to take
seriously the assertions that may be attached to a deferred feature; these assertions will be
binding on all effective versions in descendants.
For a more rigorous
The assertions of a routine, deferred or effective, specify the essential semantics of
definition see
“A
mathematical note”,
the routine, applicable not only to the routine itself but to any redeclaration in descendants.
More precisely, they specify a
range of acceptable behaviors
for the routine and its
page 580
eventual redeclarations. A redeclaration may specialize this range, but not violate it.
A consequence for the class author is the need to be careful, when writing the
assertions of an effective routine, not to
overspecify.
The assertions must characterize the
intent of the routine — its abstract semantics —, not the properties of the original
implementation. If you overspecify, you may be closing off the possibility for a future
descendant to provide a different implementation.
An example
Assume I write a class
MATRIX
implementing linear algebra operations. Among the
features I offer to my clients is a matrix inversion routine. It is actually a combination of a
command and two queries: procedure
invert
inverts the matrix, and sets attribute
inverse
to
the value of the inverse matrix, as well as a boolean attribute
inverse_valid.
The value of
inverse
is meaningful if and only if
inverse_valid
is true; otherwise the inversion has failed
because the matrix was singular. For this discussion we can ignore the singularity case.
Zgłoś jeśli naruszono regulamin