Inheritance provides a convenient mechanism to
factor out code and to use this code as basic building
blocks from which the system may be composed.
The difference between classes and procedures
in this respect is the support offered by
classes for encapsulation
and modularization.
In addition, inheritance is an important
mechanism for structuring the design specification.
From the perspective of design, inheritance
may be regarded as determining
the type structure of the system.
A type defines in an abstract way a collection of
objects with similar behavior.
This collection includes the collections of objects
defined by its subtypes.
A subtype may be regarded as imposing additional
constraints and hence as corresponding to
a generally smaller and more precisely delineated
collection of objects.
Type = objects with similar behavior
partial types are designed to have subtypes
Inheritance -- factor out code (building blocks)
abstract interface -- implementation is left to subtypes
type hierarchy -- behavioral refinement and extension
An important notion for understanding the use
of types in an object oriented context is
the notion of partial types as introduced in [HOB87].
See slide 3-partial.
Partial types are types designed to have subtypes!
Other authors speak of abstract types or abstract classes
(cf. Stroustrup, 1988; Meyer, 1988).
A partial type (or abstract class) specifies the
interface of an object (class), and hence of all
its subtypes (subclasses), without necessarily providing
a full implementation.
The actual realization of the type is left
to the object classes implementing a subtype.
As an example of a partial type, think of
an interface specification of a stack.
An abstract class may specify the interface to the stack
and derived classes may specify a variety of implementations
using, for example, fixed length arrays, linked lists
or dynamic arrays.
Another example of the use of partial types has already
been given in section inheritance, when defining a collection
of graphical shapes.
The realization of the abstract class shape is left
to the derived classes, such as circle and rectangle,
which are sufficiently concrete to provide
an implementation for the method draw.
Note that there is a significant difference
between the two examples given.
In the example of a stack, a realization amounts
simply to providing an implementation.
In the second example, however, the various realizations
of the abstract graphical shape immediately
correspond to a type hierarchy of graphical shapes.
The concrete graphical shapes are
refinements of the abstract class shape,
and may further be refined into classes describing
more specific graphical shapes.
For instance, the class rectangle may be used as a
base class for the class square.
It is also possible that a derived class extends the
interface of the base class, as
has occurred for the compound shape class.
Issues -- delegation versus inheritance
applicability -- how relevant to the type?
complexity -- how difficult to understand and implement?
A number of issues play a role in deciding
where to put the functionality when constructing
a (refinement) type hierarchy.
See slide 3-issues.
First, it must be decided whether inheritance
is the proper mechanism to use.
Often, the reuse of code is more safely effected when
using delegation or forwarding to an embedded object,
instead of inheritance. See section del-inh.
Inheritance should only be used when it can be shown
that there is actually a (behavioral) refinement
relation satisfying the constraints outlined below.
Whether particular code belongs to a type obviously
depends upon whether the functionality expressed
by the code is relevant to the type.
Another criterion concerns the complexity of the resulting
type structure, including both the cognitive complexity
and the complexity of implementing it.
In other words,
how difficult is it to understand, respectively
implement, the type hierarchy?
More particularly, what tricks, such as explicit type
conversions (casts) or the bypassing of encapsulation (friends),
must be used to realize the desired functionality in code?
Another important criterion is whether the type structure
(and code) is easily reusable.
How much effort is involved in adapting the structure
to more specific needs?
Admittedly, the solution to these issues will
depend upon the application.
However, the constraints that follow from
extending the notion of contracts to include
classes derived by inheritance do provide
a (minimal) guideline for designing a well-behaved
type structure.
Contracts and inheritance
Contracts provide a means to specify the behavior of an object
in a formal way by using logical assertions.
In particular, a contract specifies the constraints involved
in the interaction between a server object and a client invoking a method
for that object.
When developing a refinement subtype hierarchy we need to establish
that the derived types satisfy the constraints imposed by
the contract associated with the base type.
To establish that the contract of a derived class refines the contract
of the base class it suffices to verify that the following rules
are satisfied.
See slide 3-inheritance.
Refining a contract -- state responsibilities and obligations
invariance -- the invariants of all the parents of a class apply to the class itself
First, the invariant of the base class must apply to all instances
of the derived class.
In other words, the invariance assertions of the derived class must
be logically equal to or stronger than the assertions characterizing the
invariant properties of the base class.
This requirement may be verified by checking that the invariance properties
of the base class can be logically derived from the statement asserting
the invariance properties of the derived class.
The intuition underlying this requirement is that the behavior
of the derived class is more tightly defined and hence subject
to stronger invariance conditions.
Secondly, each method occurring in the base class must occur
in the derived class, possibly in a refined form.
Note that from a type theoretical point of view it is perfectly
all right to add methods but strictly forbidden to delete methods,
since deleting a method would violate the requirement of
behavioral conformance that adheres to the subtype relation.
Apart from adding a method, we may also refine existing methods.
Refining a method involves strengthening the post-condition and
weakening the pre-condition.
Suppose that we have a class C derived from a base class P,
to verify that the method refines the method defined
for the base class P, we must check, assuming that the
signatures of and are compatible, that the post-condition
of is not weaker than the post-condition of ,
and also that the pre-condition of is not stronger than the pre-condition of .
See slide 3-refining.
Refining a method -- like improving a business contract
This rule may at first sight be surprising, because
of the asymmetric way in which post-conditions and pre-conditions are treated.
But reflecting on what it means to improve
a service, the intuition underlying this rule,
and in particular the contra-variant relation
between
the pre-conditions involved, is quite
straightforward.
To improve or refine a service, in our
common sense notion of a service,
means that the quality of the product or
the result delivered becomes better.
Alternatively, a service may be considered as
improved when, even with the result remaining
the same, the cost of the service is decreased.
In other words, a service is improved if either
the client may have higher expectations of the result
or the requirements on the client becomes less
stringent.
The or is non-exclusive.
A derived class may improve a service while
at the same time imposing fewer constraints on the
clients.