In this section, we will look at some simple examples in Java that illustrate how we may use the mechanisms of inheritance and (simple) delegation to define objects that have similar functionality but differ in the way that functionality is realized. These examples prepare the way for the more complex idioms and patterns presented later in this chapter.
In the rest of this section we will look briefly at
the polymorphic constructs offered by C++.
We will also study how behavioral conformance can
be enforced in C++ by including invariants and assertions.
These sections may be skipped on first reading.
Inheritance and delegation in Java
Consider the example below,
an envelope class
that offers a message method.
In this form it is nothing but a variation on the hello world
example presented in the appendix.
public envelope() { }
public void message() {
System.out.println("hello ... ");
}
};
We will proceed in three steps: (1) The envelope class will be redesigned so that it acts only as an interface to the letter implementation class. (2) Then we introduce a factory object, that is used to create envelope and letter instances. (3) Finally, we refine the letter class into a singleton class, that prevents the creation of multiple letter instances.
public letter() { }
public void message() {
System.out.println("Message in a letter");
}
};
Factory
STL is supported by C++ compilers that adhere to the C++
standard, including Microsoft Visual C++ and the Cygnus/GNU C++ compilers.
A more extensive discussion of STL is beyond the scope of this
book, but the reader is advised to consult
In the example below, assertions are used to check
for the satisfying of both the pre-
and post-conditions of a function that computes
the square root of its argument, employing a method
known as Newton iteration.
Whereas Eiffel directly supports the use of assertions
by allowing access to the value of an instance variable
before the execution of a method through the keyword old,
the C++ programmer must rely on explicit programming
to be able to compare the state before an operation
with the state after the operation.
Assertions may also be used to check whether the
object is correctly initialized.
The pre-condition stated in the constructor
requires that the counter must start with
a value not less than zero.
In addition, the constructor checks whether
the class invariant, stated in the (virtual)
member function invariant, is satisfied.
Similarly, after checking whether the post-condition
of the
From a formal perspective, the use of assertions may
be regarded as a way of augmenting the type system supported
by object-oriented languages.
More importantly, from a software engineering perspective,
the use of assertions is a means to enforce contractual obligations.
Having studied the mechanisms, the next step is to find proper ways,
recipes as it were, to use these mechanisms.
What we need, in the terminology of
In this section, we will look at the concrete class idiom for C++,
which states the ingredients that every class must have
to behave as if it were a built-in type.
Other idioms, in particular an improved version of
the handle/body or envelope/letter idiom
that may be used to separate interface from implementation,
will be treated in the next section.
Abstract data types must be indistinguishable
from built-in types
To verify whether a concrete data type meets
the requirements imposed by the specification of
the abstract data type is quite straightforward,
although not always easy.
However, the task of verifying whether a
concrete data type is optimally implemented
is rather less well defined.
To arrive at an optimal implementation may involve
a lot of skill and ingenuity, and in general
it is hard to decide whether the right choices have been
made.
Establishing trade-offs and making choices,
for better or worse, is a matter of experience,
and crucially depends upon the skill in handling
the tools and mechanisms available.
When defining concrete data types, the list of
requirements defining the canonical class idiom
given in
slide 2-canonical
may be used as a check list to determine whether all
the necessary features of a class have been defined.
Ultimately, the programmer should strive
to realize abstract data types in such a way
that their behavior is in some sense
indistinguishable from the behavior of the built-in data types.
Since this may involve a lot of work, this need not be a primary aim in the first
stages of a software development project.
But for class libraries to work properly, it is simply essential.
draft version 0.1 (15/7/2001) Assertions in C++
Whatever support a language may offer,
reliable software is to a large extent the
result of a disciplined approach to
programming.
The use of assertions has long since been
recognized as a powerful way in which to check whether
the functional behavior of a program corresponds
with its intended behavior.
In effect, many programming language environments
support the use of assertions in some way.
For example, both C and C++ define a macro assert
which checks for the result of a boolean expression
and stops the execution if the expression is false.
require ( arg >= 0 );
double r=arg, x=1, eps=0.0001;
while( fabs(r - x) > eps ) {
r=x; x=r-((r*r-arg)/(2*r));
}
promise ( r - arg * arg <= eps );
return r;
}
public:
counter(int n = 0) : _n(n) {
require( n >= 0 );
promise( invariant() ); // check initial state
}
virtual void operator++() {
require( true ); // empty pre-condition
hold(); // save the previous state
_n += 1;
promise( _n == old_n + 1 && invariant() );
}
int value() const { return _n; } // no side effects
virtual bool invariant() { return value() >= 0; }
protected:
int _n;
int old_n;
virtual void hold() { old_n = n; }
};
public:
bounded(int b = MAXINT) : counter(0), max(b) {}
void operator++() {
require( value() < max ); // to prevent overflow
counter::operator++();
}
bool invariant() {
return value() <= max && counter::invariant();
}
private:
int max;
};
Canonical class idioms
The multitude of constructs available in C++ to support
object-oriented programming may lead the reader to think
that object-oriented programming is not at all meant
to reduce the complexity of programming but rather to increase it,
for the joy of programming so to speak.
This impression is partly justified, since the number and complexity
of constructs is at first sight indeed slightly bewildering.
However, it is necessary to realize that each of the constructs introduced
(classes, constructors and destructors, protection mechanisms,
type conversion, overloading, virtual functions and dynamic binding)
may in some way be essential to support object-oriented programming
in a type-safe,
and yet convenient, way.
Concrete data types in C++
A concrete data type is the realization of an abstract
data type.
When a concrete data type is correctly implemented
it must satisfy the requirements imposed
by the definition of the abstract data type it
realizes.
These requirements specify what operations are
defined for that type, and also
their effects.
In principle, these requirements may be formally
specified, but in practice just an informal
description is usually given.
Apart from the demands imposed by a more abstract
view of the functionality of the type, a programmer
usually also wishes to meet other requirements,
such as speed, efficiency in terms of storage
and error conditions, to prevent the removal of an item
from an empty stack, for example.
The latter requirements may be characterized
as requirements imposed by implementation concerns,
whereas the former generally result
from design considerations.
Canonical class in C++
[]
readme
course
preface
1
2
3
4
5
6
7
8
9
10
11
12
appendix
lectures
resources
eliens@cs.vu.nl