|
|||||
1:
Introduction to Objects
The
genesis of the computer
revolution was in a
machine.
The genesis of our programming
languages
thus
tends to look like that
machine.
23
But
computers are not so much machines as
they are mind
amplification
tools ("bicycles for the mind," as
Steve Jobs is fond of
saying)
and a different kind of expressive
medium. As a result,
the
tools
are beginning to look less
like machines and more like
parts of
our
minds, and also like other
expressive mediums such as
writing,
painting,
sculpture, animation, and filmmaking.
Object-oriented
programming
is part of this movement toward
using the computer
as
an expressive medium.
This
chapter will introduce you to the
basic concepts of
object-
oriented
programming (OOP), including an
overview of OOP
development
methods. This chapter, and
this book, assume
that
you
have had experience in a procedural
programming language,
although
not necessarily C. If you think you need
more preparation
in
programming and the syntax of C
before tackling this book,
you
should
work through the "Thinking in C: Foundations for C++
and
Java"
training CD ROM, bound in with this book
and also available
at
.
This
chapter is background and supplementary
material. Many
people
do not feel comfortable wading into
object-oriented
programming
without understanding the big
picture first. Thus,
there
are many concepts that are
introduced here to give you
a
solid
overview of OOP. However, many other
people don't get the
big
picture concepts until they've
seen some of the mechanics
first;
these
people may become bogged down and
lost without some
code
to get their hands on. If
you're part of this latter group and
are
eager
to get to the specifics of
the language, feel free to
jump past
this
chapter skipping it at this
point will not prevent you from
writing
programs or learning the
language. However, you will
want
to come back here eventually
to fill in your knowledge so you
can
understand why objects are
important and how to design with
them.
24
Thinking
in C++
The
progress of abstraction
All
programming languages provide
abstractions. It can be
argued
that
the complexity of the
problems you're able to solve is
directly
related
to the kind and quality of abstraction.
By "kind" I mean,
"What
is it that you are abstracting?"
Assembly language is a
small
abstraction
of the underlying machine. Many
so-called
"imperative"
languages that followed
(such as Fortran, BASIC,
and
C)
were abstractions of assembly
language. These languages
are big
improvements
over assembly language, but
their primary
abstraction
still requires you to think in terms of
the structure of the
computer
rather than the structure of
the problem you are trying
to
solve.
The programmer must establish
the association between
the
machine
model (in the "solution
space," which is the place
where
you're
modeling that problem, such
as a computer) and the
model
of
the problem that is actually
being solved (in the
"problem
space,"
which is the place where the
problem exists). The
effort
required
to perform this mapping, and
the fact that it is
extrinsic to
the
programming language, produces
programs that are difficult
to
write
and expensive to maintain, and as a side
effect created the
entire
"programming methods"
industry.
The
alternative to modeling the
machine is to model the
problem
you're
trying to solve. Early languages
such as LISP and APL
chose
particular
views of the world ("All problems
are ultimately lists"
or
"All
problems are algorithmic").
PROLOG casts all problems
into
chains
of decisions. Languages have
been created for
constraint-
based
programming and for programming
exclusively by
manipulating
graphical symbols. (The
latter proved to be
too
restrictive.)
Each of these approaches is a
good solution to the
particular
class of problem they're
designed to solve, but when you
step
outside of that domain they
become awkward.
The
object-oriented approach goes a
step farther by providing
tools
for
the programmer to represent
elements in the problem
space.
This
representation is general enough
that the programmer is
not
constrained
to any particular type of problem. We
refer to the
1:
Introduction to Objects
25
elements
in the problem space and
their representations in
the
solution
space as "objects." (Of course, you will
also need other
objects
that don't have problem-space
analogs.) The idea is that
the
program
is allowed to adapt itself to
the lingo of the problem
by
adding
new types of objects, so when you read
the code describing
the
solution, you're reading words that
also express the
problem.
This
is a more flexible and powerful language
abstraction than
what
we've had before. Thus, OOP
allows you to describe
the
problem
in terms of the problem,
rather than in terms of
the
computer
where the solution will run.
There's still a
connection
back
to the computer, though.
Each object looks quite a
bit like a
little
computer; it has a state, and it
has operations that you can
ask
it
to perform. However, this doesn't
seem like such a bad
analogy
to
objects in the real world; they all
have characteristics and
behaviors.
Some
language designers have
decided that
object-oriented
programming
by itself is not adequate to easily
solve all
programming
problems, and advocate the
combination of various
Alan
Kay summarized five basic
characteristics of Smalltalk,
the
first
successful object-oriented language and
one of the languages
upon
which C++ is based. These characteristics
represent a pure
approach
to object-oriented programming:
1.
Everything
is an object.
Think
of an object as a fancy
variable;
it stores data, but you can
"make requests" to
that
object,
asking it to perform operations on
itself. In theory,
you
can take any conceptual
component in the
problem
you're
trying to solve (dogs, buildings,
services, etc.) and
represent
it as an object in your program.
2.
A
program is a bunch of objects telling each
other
what to do by sending messages make
a
.
To
1
See Multiparadigm
Programming in Leda by Timothy
Budd (Addison-Wesley 1995).
26
Thinking
in C++
request
of an object, you "send a message" to
that object.
More
concretely, you can think of a message as
a request to
call
a function that belongs to a
particular object.
3.
Each
object has its own memory made up of
other
objects Put
another way, you create a new kind
of
.
object
by making a package containing
existing objects.
Thus,
you
can build complexity in a program while
hiding it
behind
the simplicity of
objects.
4.
Every
object has a typeUsing
the parlance, each
object
.
is
an instance
of a class, in which
"class" is synonymous with
"type."
The most important
distinguishing characteristic of a
class
is "What messages can you send to
it?"
5.
All
objects of a particular type can receive the
same
messages This
is actually a loaded statement,
as
.
you
will see later. Because an
object of type "circle" is
also an
object
of type "shape," a circle is guaranteed
to accept shape
messages.
This means you can write
code that talks to
shapes
and
automatically handles anything
that fits the
description
of
a shape. This substitutability
is one of
the most powerful
concepts
in OOP.
An
object has an interface
Aristotle
was probably the first to
begin a careful study of
the
concept
of type; he spoke
of "the class of fishes and the
class of
birds."
The idea that all objects,
while being unique, are also
part of
a
class of objects that have
characteristics and behaviors in
common
was
used directly in the first
object-oriented language,
Simula-67,
with
its fundamental keyword class
that
introduces a new type into
a
program.
1:
Introduction to Objects
27
Simula,
as its name implies, was
created for developing
simulations
of
tellers, customers, accounts,
transactions, and units of money
a
lot
of "objects." Objects that
are identical except for
their state
during
a program's execution are
grouped together into "classes
of
objects"
and that's where the keyword
class
came
from. Creating
abstract
data types (classes) is a
fundamental concept in
object-
oriented
programming. Abstract data
types work almost
exactly
like
built-in types: You can
create variables of a type
(called objects
or
instances
in
object-oriented parlance) and manipulate
those
variables
(called sending
messages or requests; you send
a message
and
the object figures out what to do with
it). The members
(elements)
of each class share some
commonality: every
account
has
a balance, every teller can
accept a deposit, etc. At
the same
time,
each member has its own
state, each account has a
different
balance,
each teller has a name.
Thus, the tellers,
customers,
accounts,
transactions, etc., can each
be represented with a unique
entity
in the computer program.
This entity is the object,
and each
object
belongs to a particular class
that defines its
characteristics
and
behaviors.
So,
although what we really do in
object-oriented programming is
create
new data types, virtually all
object-oriented programming
languages
use the "class" keyword.
When you see the word "type"
Since
a class describes a set of
objects that have
identical
characteristics
(data elements) and behaviors
(functionality), a class
is
really a data type because a
floating point number, for
example,
also
has a set of characteristics and
behaviors. The difference is
that
a
programmer defines a class to fit a
problem rather than
being
forced
to use an existing data type
that was designed to
represent a
2
You can find an interesting
implementation of this problem in
Volume 2 of this
book,
available at .
3
Some people make a
distinction, stating that
type determines the
interface while
class
is a particular implementation of that
interface.
28
Thinking
in C++
unit
of storage in a machine. You extend
the programming
language
by adding new data types
specific to your needs.
The
programming
system welcomes the new classes and
gives them all
the
care and type-checking that it
gives to built-in
types.
The
object-oriented approach is not limited
to building simulations.
Whether
or not you agree that any program is a
simulation of the
system
you're designing, the use of
OOP techniques can
easily
reduce
a large set of problems to a
simple solution.
Once
a class is established, you can
make as many objects of
that
class
as you like, and then manipulate those
objects as if they are
the
elements that exist in the
problem you are trying to
solve.
Indeed,
one of the challenges of
object-oriented programming is to
create
a one-to-one mapping between
the elements in the
problem
space
and objects in the solution
space.
But
how do you get an object to do useful
work for you? There
must
be a way to make a request of the
object so that it will do
something,
such as complete a transaction, draw
something on the
screen
or turn on a switch. And each object
can satisfy only
certain
requests.
The requests you can make of
an object are defined by
its
interface, and the
type is what determines the interface. A
simple
example
might be a representation of a light
bulb:
Light
Type
Name
on()
off()
Interface
brighten()
dim()
Light
lt;
lt.on();
The
interface establishes what
requests
you can make for a
particular
object. However, there must be code
somewhere to
satisfy
that request. This, along
with the hidden data,
comprises the
1:
Introduction to Objects
29
implementation. From a
procedural programming standpoint,
it's
not
that complicated. A type has a
function associated with
each
possible
request, and when you make a particular
request to an
object,
that function is called.
This process is usually
summarized
by
saying that you "send a
message" (make a request) to an
object,
and
the object figures out what to do with
that message (it
executes
code).
Here,
the name of the type/class
is Light,
the name of this
particular
Light
object
is lt,
and the requests that you
can make of a
Light
object
are to turn it on, turn it off,
make it brighter or make
it
dimmer.
You create a Light
object
by declaring a name (lt)
for that
object.
To send a message to the
object, you state the name
of the
object
and connect it to the message
request with a period
(dot).
From
the standpoint of the user
of a pre-defined class, that's
pretty
much
all there is to programming with
objects.
The
diagram shown above follows
the format of the Unified
Modeling
Language (UML).
Each class is represented by a
box, with
the
type name in the top portion of
the box, any data members
that
you
care to describe in the
middle portion of the box,
and the
member
functions (the
functions that belong to
this object, which
receive
any messages you send to that
object) in the bottom
portion
of
the box. Often, only the
name of the class and the
public member
functions
are shown in UML design diagrams, and so
the middle
portion
is not shown. If you're interested only in
the class name,
then
the bottom portion doesn't
need to be shown,
either.
The
hidden implementation
It
is helpful to break up the playing
field into class
creators (those
consumers
who use the data types in
their applications). The
goal
of
the client programmer is to
collect a toolbox full of classes to
use
4
I'm indebted to my friend
Scott Meyers for this
term.
30
Thinking
in C++
for
rapid application development.
The goal of the class
creator is
to
build a class that exposes only
what's necessary to the
client
programmer
and keeps everything else
hidden. Why? Because if
it's
hidden, the client
programmer can't use it,
which means that
the
class creator can change
the hidden portion at will
without
worrying
about the impact to anyone
else. The hidden
portion
usually
represents the tender
insides of an object that
could easily
be
corrupted by a careless or uninformed
client programmer, so
hiding
the implementation reduces
program bugs. The concept
of
implementation
hiding cannot be
overemphasized.
In
any relationship it's important to
have boundaries that
are
respected
by all parties involved. When you create
a library, you
establish
a relationship with the client
programmer, who is also a
programmer,
but one who is putting together an
application by
using
your library, possibly to build a bigger
library.
If
all the members of a class
are available to everyone, then
the
client
programmer can do anything with
that class and there's
no
way
to enforce rules. Even though you might
really prefer that
the
client
programmer not directly manipulate
some of the members
of
your
class, without access control
there's no way to prevent
it.
Everything's
naked to the world.
So
the first reason for access
control is to keep client
programmers'
hands
off portions they shouldn't touch
parts that are
necessary
for
the internal machinations of
the data type but not part of
the
interface
that users need in order to
solve their particular
problems.
This
is actually a service to users
because they can easily see
what's
important
to them and what they can ignore.
The
second reason for access
control is to allow the library
designer
to
change the internal workings
of the class without worrying
about
how it will affect the client
programmer. For example,
you
might
implement a particular class in a
simple fashion to
ease
development,
and then later discover that you
need to rewrite it in
order
to make it run faster. If the
interface and implementation
are
1:
Introduction to Objects
31
clearly
separated and protected, you can
accomplish this easily
and
require
only a relink by the
user.
C++
uses three explicit keywords
to set the boundaries in a
class:
public,
private,
and protected
Their
use and meaning are
quite
.
straightforward.
These access
specifiers determine
who can use the
definitions
that follow. public
means
the following definitions
are
available
to everyone. The private
keyword,
on the other hand,
means
that no one can access
those definitions except you,
the
creator
of the type, inside member
functions of that type.
private
is
a
brick wall between you and the
client programmer. If
someone
tries
to access a private
member,
they'll get a compile-time
error.
protectedacts
just like private,
with the exception that
an
inheriting
class has access to
protectedmembers,
but not private
members.
Inheritance will be introduced
shortly.
Reusing
the implementation
Once
a class has been created and
tested, it should
(ideally)
represent
a useful unit of code. It turns out that
this reusability is
not
nearly so easy to achieve as many would
hope; it takes
experience
and insight to produce a good
design. But once you
have
such a design, it begs to be
reused. Code reuse is one of
the
greatest
advantages that object-oriented
programming languages
provide.
The
simplest way to reuse a class is to
just use an object of that
class
directly,
but you can also place an
object of that class inside
a new
class.
We call this "creating a
member object." Your new class
can
be
made up of any number and type of other
objects, in any
combination
that you need to achieve the
functionality desired in
your
new class. Because you are
composing a new class from
existing
classes, this concept is
called composition
(or
more
generally,
aggregation).
Composition is often referred to as a
"has-a"
relationship,
as in "a car has an
engine."
32
Thinking
in C++
Car
Engine
(The
above UML diagram indicates
composition with the
filled
diamond,
which states there is one
car. I will typically use a
simpler
Composition
comes with a great deal of
flexibility. The
member
objects
of your new class are usually private,
making them
inaccessible
to the client programmers who
are using the class.
This
allows
you to change those members without
disturbing existing
client
code. You can also change
the member objects at
runtime, to
dynamically
change the behavior of your
program. Inheritance,
which
is described next, does not
have this flexibility since
the
compiler
must place compile-time restrictions on
classes created
with
inheritance.
Because
inheritance is so important in
object-oriented
programming
it is often highly emphasized, and the
new
programmer
can get the idea
that inheritance should be
used
everywhere.
This can result in awkward and
overly-complicated
designs.
Instead, you should first
look to composition when
creating
new classes, since it is simpler and
more flexible. If you
take
this approach, your designs will
stay cleaner. Once you've
had
some
experience, it will be reasonably obvious
when you need
inheritance.
5
This is usually enough
detail for most diagrams,
and you don't need to
get specific
about
whether you're using
aggregation or composition.
1:
Introduction to Objects
33
Inheritance:
reusing
the interface
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 then 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
child
class)
also reflects those
changes.
Base
Derived
(The
arrow in the above UML diagram
points from the
derived
class
to the base class. As you will 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 using the
concept of base types and
34
Thinking
in C++
derived
types. A base type contains all of 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 this 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" example,
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,
such as when you want
to
calculate the area of a
shape. The type hierarchy
embodies both
the
similarities and differences between
the shapes.
1:
Introduction to Objects
35
Shape
draw()
erase()
move()
getColor()
setColor()
Circle
Square
Triangle
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
previous
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.
36
Thinking
in C++
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.
Shape
draw()
erase()
move()
getColor()
setColor()
Circle
Square
Triangle
FlipVertical()
FlipHorizontal()
1:
Introduction to Objects
37
Although
inheritance may sometimes imply that you
are going to
add
new functions to the interface,
that's not necessarily true.
The
second
and more important 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.
Shape
draw()
erase()
move()
getColor()
setColor()
Circle
Square
Triangle
draw()
draw()
draw()
erase()
erase()
erase()
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
38
Thinking
in C++
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 to
determine
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.
Controls
Thermostat
Cooling
System
lowerTemperature()
cool()
Air
Conditioner
Heat
Pump
cool()
cool()
heat()
Of
course, once you see this
design it becomes clear that
the base
class
"cooling system" is not general
enough, and should be
1:
Introduction to Objects
39
renamed
to "temperature control system" so
that it can also
include
heating
at which point the substitution
principle will work.
However,
the diagram above is an
example of what can happen
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.
Interchangeable
objects
with
polymorphism
When
dealing with type hierarchies, you often
want to treat an
object
not as the specific type that it is but
instead as its base
type.
This
allows you to write code that
doesn't depend on specific
types.
In
the shape example, functions
manipulate generic shapes
without
respect
to whether they're circles,
squares, triangles, and so on.
All
shapes
can be drawn, erased, and moved, so
these functions simply
send
a message to a shape object; they don't
worry about how the
object
copes with the
message.
Such
code is unaffected by the
addition of new types, and
adding
new
types is the most common way
to extend an object-oriented
program
to handle new situations. For
example, you can derive
a
new
subtype of shape called
pentagon without modifying
the
functions
that deal only with generic
shapes. This ability to
extend
a
program easily by deriving new
subtypes is important because
it
greatly
improves designs while reducing
the cost of software
maintenance.
There's
a problem, however, with attempting to
treat derived-type
objects
as their generic base types
(circles as shapes, bicycles
as
vehicles,
cormorants as birds, etc.). If a
function is going to tell
a
40
Thinking
in C++
generic
shape to draw itself, or a generic
vehicle to steer, or a
generic
bird to move, the compiler
cannot know at compile-time
precisely
what piece of code will be executed.
That's the whole
point
when the message is sent,
the programmer doesn't
want
to
know
what piece of code will be executed;
the draw function can
be
applied
equally to a circle, a square, or a
triangle, and the
object
will
execute the proper code
depending on its specific
type. If you
don't
have to know what piece of code will be
executed, then when
you
add a new subtype, the code
it executes can be
different
without
requiring changes to the
function call. Therefore,
the
compiler
cannot know precisely what piece of
code is executed, so
what
does it do? For example, in
the following diagram
the
BirdControllerobject
just works with generic Bird
objects,
and
does
not know what exact type they are. This
is convenient from
BirdController
perspective,
because it doesn't have to
write
's
special
code to determine the exact
type of Bird
it's
working with,
or
that Bird's
behavior. So how does it happen
that, when move(
)
is
called while ignoring the
specific type of Bird,
the right behavior
will
occur (a Goose
runs,
flies, or swims, and a Penguin
runs
or
swims)?
BirdController
Bird
What
happens
reLocate()
move()
when
move() is
called?
Goose
Penguin
move()
move()
The
answer is the primary twist in
object-oriented programming:
The
compiler cannot make a
function call in the
traditional sense.
The
function call generated by a
non-OOP compiler causes what
is
called
early
binding, a term you may not
have heard before
because
you've
never thought about it any other way. It
means the compiler
generates
a call to a specific function
name, and the linker
resolves
1:
Introduction to Objects
41
this
call to the absolute address
of the code to be executed. In
OOP,
the
program cannot determine the
address of the code
until
runtime,
so some other scheme is
necessary when a message is
sent
to
a generic object.
To
solve the problem,
object-oriented languages use
the concept of
late
binding. When you
send a message to an object,
the code being
called
isn't determined until runtime.
The compiler does
ensure
that
the function exists and
performs type checking on
the
arguments
and return value (a language in which
this isn't true is
called
weakly
typed), but it
doesn't know the exact code
to execute.
To
perform late binding, the
C++ compiler inserts a special
bit of
code
in lieu of the absolute
call. This code calculates
the address of
the
function body, using
information stored in the
object (this
process
is covered in great detail in
Chapter 15). Thus, each
object
can
behave differently according to
the contents of that special
bit
of
code. When you send a message to an
object, the object
actually
does
figure out what to do with that
message.
You
state that you want a function to
have the flexibility of
late-
binding
properties using the keyword
virtual.
You don't need to
understand
the mechanics of virtual
to
use it, but without it you
can't
do object-oriented programming in C++. In
C++, you must
remember
to add the virtual
keyword
because, by default,
member
functions
are not
dynamically
bound. Virtual functions allow
you
to
express the differences in
behavior of classes in the
same family.
Those
differences are what cause
polymorphic behavior.
Consider
the shape example. The
family of classes (all based on
the
same
uniform interface) was diagrammed
earlier in the chapter.
To
demonstrate
polymorphism, we want to write a single
piece of
code
that ignores the specific
details of type and talks only to
the
base
class. That code is decoupled
from
type-specific information,
and
thus is simpler to write and easier to
understand. And, if a new
type
a Hexagon,
for example is added through
inheritance, the
42
Thinking
in C++
code
you write will work just as well for the new
type of Shape
as
it
did on the existing types.
Thus, the program is extensible.
If
you write a function in C++ (as you will
soon learn how to
do):
void
doStuff(Shape& s) {
s.erase();
//
...
s.draw();
}
This
function speaks to any Shape,
so it is independent of the
specific
type of object that it's drawing and
erasing (the `&'
means
"Take
the address of the object
that's passed to doStuff(
) but
it's
,"
not
important that you understand
the details of that right now).
If
in
some other part of the
program we use the doStuff(
)function:
Circle
c;
Triangle
t;
Line
l;
doStuff(c);
doStuff(t);
doStuff(l);
The
calls to doStuff(
)automatically
work right, regardless of
the
exact
type of the object.
This
is actually a pretty amazing
trick. Consider the
line:
doStuff(c);
What's
happening here is that a
Circle
is
being passed into a
function
that's expecting a Shape.
Since a Circle
is a
Shape
it
can be
treated
as one by doStuff(
) That
is, any message that
doStuff(
)
.
can
send to a Shape,
a Circle
can
accept. So it is a completely
safe
and
logical thing to do.
We
call this process of
treating a derived type as though it were
its
base
type upcasting. The
name cast
is used in
the sense of casting
into
a mold and the up
comes from
the way the inheritance
diagram
is
typically arranged, with the base type at
the top and the
derived
1:
Introduction to Objects
43
classes
fanning out downward. Thus, casting to a
base type is
moving
up the inheritance diagram:
"upcasting."
Shape
"Upcasting"
Circle
Square
Triangle
An
object-oriented program contains
some upcasting
somewhere,
because
that's how you decouple yourself from
knowing about the
exact
type you're working with. Look at the
code in doStuff(
)
:
s.erase();
//
...
s.draw();
Notice
that it doesn't say "If you're a
Circle,
do this, if you're a
Square,
do that, etc." If you write that kind of
code, which checks
for
all the possible types that
a Shape
can
actually be, it's
messy
and
you need to change it every
time you add a new kind of Shape.
Here,
you just say "You're a shape, I know you
can erase(
)and
draw(
) yourself,
do it, and take care of the
details correctly."
What's
impressive about the code in
doStuff(
)is
that, somehow,
the
right thing happens. Calling draw(
) for
Circle
causes
different
code
to be executed than when calling
draw(
) for
a Square
or
a
Line,
but when the draw(
) message
is sent to an anonymous
Shape,
the correct behavior occurs
based on the actual type of
the
Shape.
This is amazing because, as
mentioned earlier, when
the
C++
compiler is compiling the
code for doStuff(
) it
cannot know
,
exactly
what types it is dealing with. So
ordinarily, you'd expect it
to
end up calling the version
of erase(
)and
draw(
) for
Shape,
and
not
for the specific Circle,
Square,
or Line.
And yet the right thing
happens
because of polymorphism. The
compiler and runtime
44
Thinking
in C++
system
handle the details; all you
need to know is that it
happens
and
more importantly how to design with
it. If a member
function
is
virtual,
then when you send a message to an
object, the object
will
do the right thing, even when
upcasting is involved.
Creating
and destroying objects
Technically,
the domain of OOP is
abstract data typing,
inheritance,
and
polymorphism, but other issues
can be at least as
important.
This
section gives an overview of
these issues.
Especially
important is the way objects
are created and
destroyed.
Where
is the data for an object and how is
the lifetime of that
object
controlled?
Different programming languages
use different
philosophies
here. C++ takes the approach
that control of
efficiency
is
the most important issue, so
it gives the programmer a
choice.
For
maximum runtime speed, the storage and
lifetime can be
determined
while the program is being
written, by placing
the
objects
on the stack or in static
storage. The stack is an
area in
memory
that is used directly by the
microprocessor to store
data
during
program execution. Variables on
the stack are
sometimes
called
automatic
or scoped
variables.
The static storage area is
simply
a
fixed patch of memory that is
allocated before the program
begins
to
run. Using the stack or
static storage area places a
priority on the
speed
of storage allocation and release, which
can be valuable in
some
situations. However, you sacrifice
flexibility because you
must
know the exact quantity,
lifetime, and type of objects while
you're
writing the program. If you are trying to
solve a more
general
problem, such as computer-aided
design, warehouse
management,
or air-traffic control, this is
too restrictive.
The
second approach is to create
objects dynamically in a pool
of
memory
called the heap. In this
approach you don't know until
runtime
how many objects you need, what their
lifetime is, or what
their
exact type is. Those
decisions are made at the
spur of the
moment
while the program is running. If you need
a new object,
you
simply make it on the heap when you
need it, using the
new
1:
Introduction to Objects
45
keyword.
When you're finished with the
storage, you must release
it
using the delete
keyword.
Because
the storage is managed
dynamically at runtime,
the
amount
of time required to allocate
storage on the heap
is
significantly
longer than the time to
create storage on the
stack.
(Creating
storage on the stack is
often a single
microprocessor
instruction
to move the stack pointer down, and
another to move it
back
up.) The dynamic approach
makes the generally
logical
assumption
that objects tend to be
complicated, so the
extra
overhead
of finding storage and releasing
that storage will not
have
an
important impact on the
creation of an object. In addition,
the
greater
flexibility is essential to solve
general programming
problems.
There's
another issue, however, and
that's the lifetime of an
object.
If
you create an object on the
stack or in static storage,
the compiler
determines
how long the object lasts and
can automatically
destroy
it.
However, if you create it on the heap,
the compiler has no
knowledge
of its lifetime. In C++, the
programmer must determine
programmatically
when to destroy the object, and then
perform the
destruction
using the delete
keyword.
As an alternative, the
environment
can provide a feature called
a garbage
collector that
automatically
discovers when an object is no longer in
use and
destroys
it. Of course, writing programs
using a garbage collector
is
much
more convenient, but it requires
that all applications must be
able
to tolerate the existence of
the garbage collector and
the
overhead
for garbage collection. This
does not meet the
design
requirements
of the C++ language and so it was not
included,
although
third-party garbage collectors
exist for C++.
Exception
handling:
dealing
with errors
Ever
since the beginning of
programming languages,
error
handling
has been one of the
most difficult issues.
Because it's so
46
Thinking
in C++
hard
to design a good error-handling
scheme, many languages
simply
ignore the issue, passing
the problem on to library
designers
who
come up with halfway measures that
can work in many
situations
but can easily be circumvented,
generally by just
ignoring
them. A major problem with
most error-handling
schemes
is
that they rely on programmer vigilance in
following an agreed-
upon
convention that is not enforced by
the language. If
programmers
are not vigilant, which often
occurs when they are in
a
hurry, these schemes can
easily be forgotten.
Exception
handling wires
error handling directly into
the
programming
language and sometimes even
the operating system.
An
exception is an object that is "thrown"
from the site of the
error
and
can be "caught" by an appropriate
exception
handler designed
to
handle
that particular type of error. It's as if
exception handling is a
different,
parallel path of execution
that can be taken when
things
go
wrong. And because it uses a separate
execution path, it
doesn't
need
to interfere with your normally-executing
code. This makes
that
code simpler to write since you
aren't constantly forced
to
check
for errors. In addition, a thrown
exception is unlike an
error
value
that's returned from a function or a
flag that's set by a
function
in order to indicate an error
condition these can
be
ignored.
An exception cannot be ignored so
it's guaranteed to be
dealt
with at some point. Finally,
exceptions provide a way to
recover
reliably from a bad situation.
Instead of just exiting
the
program,
you are often able to set
things right and restore
the
execution
of a program, which produces much more
robust
systems.
It's
worth noting that exception
handling isn't an
object-oriented
feature,
although in object-oriented languages
the exception is
normally
represented with an object. Exception
handling existed
before
object-oriented languages.
Exception
handling is only lightly introduced and
used in this
Volume;
Volume 2 (available from )
has
thorough
coverage of exception
handling.
1:
Introduction to Objects
47
Analysis
and design
The
object-oriented paradigm is a new and
different way of
thinking
about programming and many folks
have trouble at first
knowing
how to approach an OOP project. Once you
know that
everything
is supposed to be an object, and as you
learn to think
more
in an object-oriented style, you can
begin to create "good"
designs
that take advantage of all
the benefits that OOP has
to
offer.
A
method
(often
called a methodology) is a set
of processes and
heuristics
used to break down the
complexity of a programming
problem.
Many OOP methods have been
formulated since the
dawn
of object-oriented programming. This
section will give you a
feel
for what you're trying to accomplish when
using a method.
Especially
in OOP, methodology is a field of many
experiments, so
it
is important to understand what problem
the method is trying to
solve
before you consider adopting
one. This is particularly
true
with
C++, in which the programming
language is intended to
reduce
the complexity (compared to C) involved
in expressing a
program.
This may in fact alleviate
the need for
ever-more-complex
methodologies.
Instead, simpler ones may
suffice in C++ for a
much
larger class of problems than you
could handle using
simple
methodologies
with procedural languages.
It's
also important to realize
that the term "methodology" is
often
too
grand and promises too much.
Whatever you do now when
you
design and write a program is a method.
It may be your own
method,
and you may not be conscious of doing it,
but it is a
process
you go through as you create. If it is an effective
process, it
may
need only a small tune-up to work with
C++. If you are not
satisfied
with your productivity and the way your
programs turn
out,
you may want to consider adopting a
formal method, or
choosing
pieces from among the many
formal methods.
While
you're going through the development
process, the most
important
issue is this: Don't get
lost. It's easy to do. Most of
the
48
Thinking
in C++
analysis
and design methods are
intended to solve the
largest of
problems.
Remember that most projects
don't fit into that category,
so
you can usually have successful
analysis and design with a
sort
of process, no matter how limited, will
generally get you on
your
way in a much better fashion than simply
beginning to code.
It's
also easy to get stuck, to
fall into "analysis paralysis,"
where
you
feel like you can't move forward
because you haven't
nailed
down
every little detail at the
current stage. Remember, no
matter
how
much analysis you do, there
are some things about a
system
that
won't reveal themselves until design
time, and more things
that
won't reveal themselves until you're
coding, or not even until
a
program is up and running. Because of
this, it's crucial to
move
fairly
quickly through analysis and design, and
to implement a test
of
the proposed system.
This
point is worth emphasizing. Because of
the history we've had
with
procedural languages, it is commendable
that a team will
want
to proceed carefully and understand
every minute detail
before
moving to design and implementation.
Certainly, when
creating
a DBMS, it pays to understand a
customer's needs
thoroughly.
But a DBMS is in a class of problems
that is very well-
posed
and well-understood; in many such
programs, the
database
structure
is
the
problem to be tackled. The
class of programming
problem
discussed in this chapter is of
the "wild-card" (my
term)
variety,
in which the solution isn't simply
re-forming a well-known
solution,
but instead involves one or
more "wild-card factors"
elements
for which there is no well-understood
previous solution,
6
An excellent example of
this is UML
Distilled, by
Martin Fowler
(Addison-Wesley
2000),
which reduces the
sometimes-overwhelming UML process to a
manageable
subset.
7
My rule of thumb for
estimating such projects: If
there's more than one
wild card,
don't
even try to plan how long
it's going to take or how
much it will cost
until
you've
created a working prototype.
There are too many degrees
of freedom.
1:
Introduction to Objects
49
analyze
a wild-card problem before moving into
design and
implementation
results in analysis paralysis
because you don't
have
enough information to solve
this kind of problem during
the
analysis
phase. Solving such a
problem requires iteration
through
the
whole cycle, and that requires
risk-taking behavior (which
makes
sense, because you're trying to do
something new and the
potential
rewards are higher). It may
seem like the risk
is
compounded
by "rushing" into a preliminary implementation,
but
it
can instead reduce the
risk in a wild-card project
because you're
finding
out early whether a particular
approach to the problem
is
viable.
Product development is risk
management.
It's
often proposed that you "build
one to throw away." With OOP,
you
may still throw part
of it away, but
because code is
encapsulated
into classes, during the first
iteration you will
inevitably
produce some useful class
designs and develop
some
worthwhile
ideas about the system
design that do not need to
be
thrown
away. Thus, the first rapid
pass at a problem not only
produces
critical information for the
next analysis, design,
and
implementation
iteration, it also creates a
code foundation for
that
iteration.
That
said, if you're looking at a
methodology that
contains
tremendous
detail and suggests many steps and
documents, it's
still
difficult to know when to stop. Keep in
mind what you're
trying
to discover:
1.
What
are the objects? (How do you
partition your project into
its
component parts?)
2.
What
are their interfaces? (What
messages do you need to be
able
to send to each
object?)
If
you come up with nothing more than
the objects and their
interfaces,
then you can write a program. For
various reasons you
might
need more descriptions and
documents than this, but you
can't
get away with any less.
50
Thinking
in C++
The
process can be undertaken in
five phases, and a phase 0
that is
just
the initial commitment to
using some kind of
structure.
Phase
0: Make a plan
You
must first decide what steps you're
going to have in your
process.
It sounds simple (in fact,
all
of this
sounds simple) and yet
people
often don't make this
decision before they start
coding. If
your
plan is "let's jump in and start coding,"
fine. (Sometimes
that's
appropriate when you have a
well-understood problem.) At
least
agree that this is the
plan.
You
might also decide at this
phase that some additional
process
structure
is necessary, but not the whole nine
yards.
Understandably
enough, some programmers
like to work in
"vacation
mode" in which no structure is imposed on
the process
of
developing their work; "It will be done
when it's done." This
can
be
appealing for awhile, but I've found that
having a few
milestones
along the way helps to focus
and galvanize your efforts
around
those milestones instead of
being stuck with the single
goal
of
"finish the project." In
addition, it divides the
project into more
bite-sized
pieces and makes it seem
less threatening (plus
the
milestones
offer more opportunities for
celebration).
When
I began to study story structure
(so that I will someday
write
a
novel) I was initially
resistant to the idea of
structure, feeling
that
when
I wrote I simply let it flow onto the
page. But I later
realized
that
when I write about computers the
structure is clear enough
so
that
I don't think much about it. But I still
structure my work, albeit
only
semi-consciously in my head. So even if
you think that your
plan
is to just start coding, you
still somehow go through
the
subsequent
phases while asking and answering
certain questions.
The
mission statement
Any
system you build, no matter how
complicated, has a
fundamental
purpose, the business that
it's in, the basic
need that it
satisfies.
If you can look past the
user interface, the
hardware- or
system-specific
details, the coding
algorithms and the
efficiency
1:
Introduction to Objects
51
problems,
you will eventually find the core of
its being, simple and
straightforward.
Like the so-called high
concept from a
Hollywood
movie,
you can describe it in one or two
sentences. This pure
description
is the starting
point.
The
high concept is quite important
because it sets the tone for
your
project;
it's a mission statement. You won't
necessarily get it right
the
first time (you may be in a later
phase of the project before
it
becomes
completely clear), but keep trying until
it feels right. For
example,
in an air-traffic control system you may
start out with a
high
concept focused on the
system that you're building:
"The
tower
program keeps track of the
aircraft." But consider what
happens
when you shrink the system to a very
small airfield;
perhaps
there's only a human controller or none
at all. A more
useful
model won't concern the
solution you're creating as much
as
it
describes the problem:
"Aircraft arrive, unload,
service and
reload,
and depart."
Phase
1: What are we making?
In
the previous generation of
program design (called
procedural
design), this is
called "creating the requirements
analysis and system
specification." These,
of course, were places to
get lost;
intimidatingly-named
documents that could become
big projects in
their
own right. Their intention
was good, however.
The
requirements
analysis says "Make a list of
the guidelines we will
use
to know when the job is done and
the customer is
satisfied."
The
system specification says
"Here's a description of what
the
program
will do (not how) to
satisfy the requirements."
The
requirements
analysis is really a contract
between you and the
customer
(even if the customer works within your
company or is
some
other object or system). The
system specification is a
top-level
exploration
into the problem and in some
sense a discovery of
whether
it can be done and how long it will take.
Since both of
these
will require consensus among
people (and because they
will
usually
change over time), I think
it's best to keep them as
bare as
possible
ideally, to lists and basic
diagrams to save time.
You
52
Thinking
in C++
might
have other constraints that
require you to expand them into
bigger
documents, but by keeping the
initial document small
and
concise,
it can be created in a few sessions of
group brainstorming
with
a leader who dynamically creates
the description. This
not
only
solicits input from everyone, it also
fosters initial buy-in and
agreement
by everyone on the team.
Perhaps most importantly,
it
can
kick off a project with a lot of
enthusiasm.
It's
necessary to stay focused on
the heart of what you're trying
to
accomplish
in this phase: determine what
the system is supposed
to
do.
The most valuable tool for
this is a collection of what are
called
"use
cases." Use cases identify
key features in the system
that will
reveal
some of the fundamental
classes you'll be using. These
are
·
"Who
will use this
system?"
·
"What
can those actors do with the
system?"
·
"How
does this actor do that with
this system?"
·
"How
else might this work if someone
else were doing
this,
or
if the same actor had a
different objective?" (to
reveal
variations)
·
"What
problems might happen while doing
this with the
system?"
(to reveal
exceptions)
If
you are designing an auto-teller, for
example, the use case for
a
particular
aspect of the functionality of
the system is able to
describe
what the auto-teller does in
every possible situation.
Each
of
these "situations" is referred to as a
scenario, and a
use case can
be
considered a collection of scenarios. You
can think of a scenario
as
a question that starts with: "What
does the system do if...?"
For
example,
"What does the auto-teller do if a
customer has just
deposited
a check within 24 hours and there's not
enough in the
account
without the check to provide
the desired
withdrawal?"
8
Thanks for help
from James H Jarrett.
1:
Introduction to Objects
53
Use
case diagrams are
intentionally simple to prevent you
from
getting
bogged down in system implementation
details
prematurely:
Bank
Make
Deposit
Uses
Make
Teller
Withdrawal
Get
Account
Customer
Balance
Transfer
Between
Accounts
ATM
Each
stick person represents an
"actor," which is typically a
human
or
some other kind of free
agent. (These can even be
other
computer
systems, as is the case with "ATM.")
The box represents
the
boundary of your system. The
ellipses represent the use
cases,
which
are descriptions of valuable work
that can be performed
with
the system. The lines
between the actors and the
use cases
represent
the interactions.
It
doesn't matter how the
system is actually implemented, as
long
as
it looks like this to the
user.
A
use case does not need to be
terribly complex, even if
the
underlying
system is complex. It is only intended to
show the
system
as it appears to the user.
For example:
54
Thinking
in C++
Greenhouse
Maintain
Growing
Temperature
Gardener
The
use cases produce the
requirements specifications by
determining
all the interactions that
the user may have with
the
system.
You try to discover a full set of use
cases for your system,
and
once you've done that you
have the core of what the
system is
supposed
to do. The nice thing about
focusing on use cases is
that
they
always bring you back to the
essentials and keep you from
drifting
off into issues that aren't
critical for getting the job
done.
That
is, if you have a full set of
use cases you can describe
your
system
and move onto the next
phase. You probably won't get it
all
figured
out perfectly on the first try, but
that's OK. Everything will
reveal
itself in time, and if you demand a
perfect system
specification
at this point you'll get
stuck.
If
you get stuck, you can
kick-start this phase by
using a rough
approximation
tool: describe the system in
a few paragraphs and
then
look for nouns and verbs.
The nouns can suggest
actors,
context
of the use case (e.g.
"lobby"), or artifacts manipulated in
the
use
case. Verbs can suggest
interactions between actors and
use
cases,
and specify steps within the
use case. You'll also
discover
that
nouns and verbs produce
objects and messages during
the
design
phase (and note that
use cases describe
interactions between
subsystems,
so the "noun and verb" technique can be
used only as
The
boundary between a use case
and an actor can point out
the
existence
of a user interface, but it does not
define such a user
9
More information on use
cases can be found in
Applying
Use Cases by
Schneider &
Winters
(Addison-Wesley 1998) and Use
Case Driven Object Modeling
with UML by
Rosenberg
(Addison-Wesley 1999).
1:
Introduction to Objects
55
interface.
For a process of defining and
creating user interfaces,
see
Software
for Use by Larry
Constantine and Lucy Lockwood,
(Addison
Wesley Longman, 1999) or go to
www.ForUse.com.
Although
it's a black art, at this
point some kind of
basic
scheduling
is important. You now have an overview of
what you're
building
so you'll probably be able to get
some idea of how long it
will
take. A lot of factors come into play
here. If you estimate a long
schedule
then the company might decide not to
build it (and thus
use
their resources on something
more reasonable that's a
good
thing).
Or a manager might have already
decided how long the
project
should take and will try to influence
your estimate. But it's
best
to have an honest schedule from
the beginning and deal
with
the
tough decisions early. There
have been a lot of attempts to
come
up
with accurate scheduling techniques
(like techniques to
predict
the
stock market), but probably
the best approach is to rely on
your
experience
and intuition. Get a gut feeling for how
long it will
really
take, then double that and
add 10 percent. Your gut feeling
is
probably
correct; you can
get
something working in that time.
The
"doubling"
will turn that into something decent, and
the 10 percent
to
explain it, and regardless of
the moans and manipulations
that
happen
when you reveal such a schedule, it
just seems to work out
that
way.
Phase
2: How will we build it?
In
this phase you must come up with a
design that describes
what
the
classes look like and how they will
interact. An excellent
technique
in determining classes and interactions
is the Class-
Responsibility-Collaboration
(CRC)
card. Part of the value of
this tool
10
My personal take on this
has changed lately. Doubling
and adding 10 percent
will
give
you a reasonably accurate
estimate (assuming there are
not too many
wild-card
factors),
but you still have to
work quite diligently to
finish in that time. If you
want
time
to really make it elegant
and to enjoy yourself in the
process, the correct
multiplier
is more like three or four
times, I believe.
56
Thinking
in C++
is
that it's so low-tech: you
start out with a set of blank 3" by
5"
cards,
and you write on them. Each card
represents a single
class,
and
on the card you
write:
1.
The
name of the class. It's
important that this name
capture
the
essence of what the class does, so
that it makes sense at
a
glance.
2.
The
"responsibilities" of the class: what it
should do. This
can
typically
be summarized by just stating
the names of the
member
functions (since those names
should be descriptive
in
a good design), but it does not
preclude other notes. If
you
need
to seed the process, look at
the problem from a lazy
programmer's
standpoint: What objects would you like
to
magically
appear to solve your
problem?
3.
The
"collaborations" of the class: what
other classes does it
interact
with? "Interact" is an intentionally
broad term; it
could
mean aggregation or simply that
some other object
exists
that will perform services for an
object of the class.
Collaborations
should also consider the
audience for this
class.
For example, if you create a
class Firecracker
who
is
,
going
to observe it, a Chemist
or
a Spectator
The
former
?
will
want to know what chemicals go into the
construction,
and
the latter will respond to
the colors and shapes
released
when
it explodes.
You
may feel like the cards
should be bigger because of all
the
information
you'd like to get on them, but they
are intentionally
small,
not only to keep your classes small but
also to keep you from
getting
into too much detail too
early. If you can't fit all you need
to
know
about a class on a small
card, the class is too
complex (either
you're
getting too detailed, or you
should create more than
one
class).
The ideal class should be
understood at a glance. The
idea of
CRC
cards is to assist you in coming up with
a first cut of the
design
so that you can get the
big picture and then refine
your
design.
1:
Introduction to Objects
57
One
of the great benefits of CRC
cards is in communication. It's
best
done real-time, in a group, without
computers. Each
person
takes
responsibility for several classes (which at
first have no
names
or other information). You run a live
simulation by solving
one
scenario at a time, deciding which
messages are sent to
the
various
objects to satisfy each
scenario. As you go through this
process,
you discover the classes that you
need along with their
responsibilities
and collaborations, and you fill out the
cards as you
do
this. When you've moved through all the
use cases, you should
have
a fairly complete first cut of your
design.
Before
I began using CRC cards,
the most successful
consulting
experiences
I had when coming up with an initial
design involved
standing
in front of a team, who hadn't built an OOP
project before,
and
drawing objects on a whiteboard. We
talked about how the
objects
should communicate with each
other, and erased some
of
them
and replaced them with other objects.
Effectively, I was
managing
all the "CRC cards" on the
whiteboard. The team
(who
knew
what the project was
supposed to do) actually
created the
design;
they "owned" the design rather than
having it given to
them.
All I was doing was guiding
the process by asking the
right
questions,
trying out the assumptions, and taking
the feedback
from
the team to modify those
assumptions. The true beauty of
the
process
was that the team
learned how to do object-oriented
design
not
by reviewing abstract examples, but by
working on the one
design
that was most interesting to
them at that moment:
theirs.
Once
you've come up with a set of CRC
cards, you may want to
don't
need to use UML, but it can be
helpful, especially if you want
to
put up a diagram on the wall for everyone
to ponder, which is a
good
idea. An alternative to UML is a textual
description of the
11
For starters, I recommend
the aforementioned UML
Distilled.
58
Thinking
in C++
objects
and their interfaces, or, depending on
your programming
UML
also provides an additional
diagramming notation for
describing
the dynamic model of your
system. This is helpful in
situations
in which the state transitions of a
system or subsystem
are
dominant enough that they
need their own diagrams
(such as in
a
control system). You may also
need to describe the
data
structures,
for systems or subsystems in which data
is a dominant
factor
(such as a database).
You'll
know you're done with phase 2 when you
have described
the
objects and their interfaces.
Well, most of them there
are
usually
a few that slip through the
cracks and don't make
themselves
known until phase 3. But that's OK. All you
are
concerned
with is that you eventually discover all
of your objects.
It's
nice to discover them early in
the process but OOP
provides
enough
structure so that it's not so
bad if you discover them
later.
In
fact, the design of an
object tends to happen in
five stages,
throughout
the process of program
development.
Five
stages of object design
The
design life of an object is not
limited to the time when
you're
writing
the program. Instead, the
design of an object appears
over a
sequence
of stages. It's helpful to have this
perspective because you
stop
expecting perfection right away;
instead, you realize that
the
understanding
of what an object does and what it should
look like
happens
over time. This view also
applies to the design of
various
types
of programs; the pattern for a
particular type of program
emerges
through struggling again and again with
that problem
(Design
Patterns are
covered in Volume 2). Objects,
too, have their
patterns
that emerge through understanding,
use, and reuse.
12
Python (www.Python.org) is
often used as "executable
pseudocode."
1:
Introduction to Objects
59
1.
Object discovery.
This
stage occurs during the
initial
analysis
of a program. Objects may be discovered
by looking for
external
factors and boundaries, duplication of
elements in the
system,
and the smallest conceptual
units. Some objects are
obvious
if
you already have a set of
class libraries. Commonality
between
classes
suggesting base classes and inheritance may
appear right
away,
or later in the design
process.
2.
Object assembly. you're
building an object you'll
As
discover
the need for new members
that didn't appear during
discovery.
The internal needs of the
object may require
other
classes
to support it.
3.
System construction.
Once
again, more requirements
for
an
object may appear at this
later stage. As you learn, you
evolve
your
objects. The need for
communication and interconnection
with
other
objects in the system may
change the needs of your classes
or
require
new classes. For example, you may
discover the need for
facilitator
or helper classes, such as a
linked list, that contain
little
or
no state information and simply help
other classes function.
4.
System extension. you
add new features to a
system
As
you
may discover that your previous design
doesn't support easy
system
extension. With this new information, you
can restructure
parts
of the system, possibly
adding new classes or class
hierarchies.
5.
Object reuse.
This
is the real stress test for
a class. If
someone
tries to reuse it in an entirely new
situation, they'll
probably
discover some shortcomings. As you
change a class to
adapt
to more new programs, the
general principles of the
class
will
become clearer, until you have a truly
reusable type. However,
don't
expect most objects from a
system design to be reusable it
is
perfectly
acceptable for the bulk of your objects
to be system-
specific.
Reusable types tend to be
less common, and they must
solve
more general problems in
order to be reusable.
60
Thinking
in C++
Guidelines
for object development
These
stages suggest some
guidelines when thinking about
developing
your classes:
1.
Let
a specific problem generate a
class, then let the class
grow
and
mature during the solution of
other problems.
2.
Remember,
discovering the classes you
need (and their
interfaces)
is the majority of the
system design. If you
already
had
those classes, this would be an
easy project.
3.
Don't
force yourself to know everything at
the beginning;
learn
as you go. This will happen
anyway.
4.
Start
programming; get something working so you
can prove
or
disprove your design. Don't fear
that you'll end up with
procedural-style
spaghetti code classes
partition the
problem
and help control anarchy and
entropy. Bad classes
do
not break good
classes.
5.
Always
keep it simple. Little clean
objects with obvious
utility
are better than big
complicated interfaces. When
decision
points come up, use an
Occam's Razor
approach:
Consider
the choices and select the
one that is simplest,
because
simple classes are almost
always best. Start
small
and
simple, and you can expand
the class interface when
you
understand
it better, but as time goes
on, it's difficult to
remove
elements from a class.
Phase
3: Build the core
This
is the initial conversion from
the rough design into a
compiling
and executing body of code that
can be tested, and
especially
that will prove or disprove your
architecture. This is not
a
one-pass process, but rather
the beginning of a series of
steps that
will
iteratively build the system, as you'll
see in phase 4.
Your
goal is to find the core of your
system architecture that
needs
to
be implemented in order to generate a
running system, no matter
1:
Introduction to Objects
61
how
incomplete that system is in
this initial pass. You're
creating a
framework
that you can build upon with further
iterations. You're
also
performing the first of many
system integrations and tests,
and
giving
the stakeholders feedback
about what their system will
look
like
and how it is progressing. Ideally, you
are also exposing
some
of
the critical risks. You'll
probably also discover
changes and
improvements
that can be made to your
original architecture
things
you would not have learned without
implementing the
system.
Part
of building the system is
the reality check that you
get from
testing
against your requirements analysis and
system specification
(in
whatever form they exist). Make
sure that your tests verify
the
requirements
and use cases. When the core
of the system is
stable,
you're
ready to move on and add more
functionality.
Phase
4: Iterate the use cases
Once
the core framework is running,
each feature set you add is
a
small
project in itself. You add a
feature set during an iteration, a
reasonably
short period of
development.
How
big is an iteration? Ideally,
each iteration lasts one to
three
weeks
(this can vary based on the
implementation language). At
the
end of that period, you have
an integrated, tested system
with
more
functionality than it had before. But
what's particularly
interesting
is the basis for the
iteration: a single use case.
Each use
case
is a package of related functionality
that you build into the
system
all at once, during one iteration. Not
only does this give
you
a
better idea of what the
scope of a use case should
be, but it also
gives
more validation to the idea
of a use case, since the
concept
isn't
discarded after analysis and
design, but instead it is a
fundamental
unit of development throughout the
software-
building
process.
You
stop iterating when you achieve
target functionality or an
external
deadline arrives and the
customer can be satisfied with
the
current
version. (Remember, software is a
subscription business.)
62
Thinking
in C++
Because
the process is iterative, you
have many opportunities to
ship
a product instead of a single
endpoint; open-source
projects
work
exclusively in an iterative,
high-feedback environment, which
is
precisely what makes them
successful.
An
iterative development process is
valuable for many reasons.
You
can reveal and resolve
critical risks early, the
customers have
ample
opportunity to change their
minds, programmer
satisfaction
is
higher, and the project can
be steered with more precision. But
an
additional
important benefit is the
feedback to the
stakeholders,
who
can see by the current
state of the product exactly
where
everything
lies. This may reduce or
eliminate the need for
mind-
numbing
status meetings and increase
the confidence and
support
from
the stakeholders.
Phase
5: Evolution
This
is the point in the
development cycle that has
traditionally
been
called "maintenance," a catch-all term
that can mean
everything
from "getting it to work the way it was
really supposed
to
in the first place" to
"adding features that the
customer forgot to
mention"
to the more traditional "fixing
the bugs that show
up"
and
"adding new features as the
need arises." So many
misconceptions
have been applied to the
term "maintenance" that
it
has taken on a slightly
deceiving quality, partly because
it
suggests
that you've actually built a pristine
program and all you
need
to do is change parts, oil it, and
keep it from rusting.
Perhaps
there's
a better term to describe what's
going on.
time,
so give yourself the
latitude to learn and to go back and
make
changes."
You might need to make a lot of changes
as you learn
and
understand the problem more
deeply. The elegance
you'll
13
At least one aspect of
evolution is covered in Martin
Fowler's book Refactoring:
improving
the design of existing code
(Addison-Wesley
1999). Be forewarned that
this
book
uses Java examples
exclusively.
1:
Introduction to Objects
63
produce
if you evolve until you get it right will pay
off, both in the
short
and the long term. Evolution is
where your program
goes
from
good to great, and where
those issues that you didn't
really
understand
in the first pass become
clear. It's also where
your
classes
can evolve from single-project
usage to reusable
resources.
What
it means to "get it right" isn't
just that the program
works
according
to the requirements and the
use cases. It also means
that
the
internal structure of the
code makes sense to you, and
feels like
it
fits together well, with no awkward
syntax, oversized objects,
or
ungainly
exposed bits of code. In
addition, you must have
some
sense
that the program structure
will survive the changes
that it
will
inevitably go through during its
lifetime, and that
those
changes
can be made easily and
cleanly. This is no small
feat. You
must
not only understand what you're building,
but also how the
object-oriented
programming languages are
particularly adept at
supporting
this kind of continuing modification
the boundaries
created
by the objects are what tend
to keep the structure
from
breaking
down. They also allow you to make changes
ones that
would
seem drastic in a procedural
program without causing
earthquakes
throughout your code. In fact, support
for evolution
might
be the most important
benefit of OOP.
With
evolution, you create something
that at least
approximates
what
you think you're building, and then you
kick the tires,
compare
it to your requirements and see where it
falls short. Then
you
can go back and fix it by redesigning and
re-implementing the
14
This term is explored in
the Design
Patterns chapter in
Volume 2.
15
This is something like
"rapid prototyping," where
you were supposed to build
a
quick-and-dirty
version so that you could
learn about the system,
and then throw
away
your prototype and build it
right. The trouble with
rapid prototyping is
that
people
didn't throw away the
prototype, but instead built
upon it. Combined
with
the
lack of structure in procedural
programming, this often
produced messy systems
that
were expensive to
maintain.
64
Thinking
in C++
actually
need to solve the problem,
or an aspect of the
problem,
several
times before you hit on the right
solution. (A study of
Design
Patterns,
described in Volume 2, is usually helpful
here.)
Evolution
also occurs when you build a system,
see that it matches
your
requirements, and then discover it wasn't
actually what you
wanted.
When you see the system in
operation, you find that you
really
wanted to solve a different
problem. If you think this kind of
evolution
is going to happen, then you owe it to
yourself to build
your
first version as quickly as
possible so you can find out if it
is
indeed
what you want.
Perhaps
the most important thing to
remember is that by default
by
definition, really if you modify a
class then its super-
and
subclasses
will still function. You need not
fear modification
(especially
if you have a built-in set of unit
tests to verify the
correctness
of your modifications). Modification won't
necessarily
break
the program, and any change in
the outcome will be
limited
to
subclasses and/or specific collaborators
of the class you
change.
Plans
pay off
Of
course you wouldn't build a house without a lot of
carefully-
drawn
plans. If you build a deck or a dog
house, your plans won't
be
so elaborate but you'll probably still
start with some kind of
sketches
to guide you on your way. Software
development has
gone
to extremes. For a long time,
people didn't have much
structure
in their development, but then big
projects began
failing.
In
reaction, we ended up with methodologies
that had an
intimidating
amount of structure and detail, primarily
intended for
those
big projects. These
methodologies were too scary
to use it
looked
like you'd spend all your time writing
documents and no
time
programming. (This was often
the case.) I hope that what
I've
shown
you here suggests a middle
path a sliding scale.
Use an
approach
that fits your needs (and
your personality). No matter
how
minimal you choose to make it,
some
kind of plan will
make a
big
improvement in your project as opposed to
no plan at all.
1:
Introduction to Objects
65
Remember
that, by most estimates,
over 50 percent of projects
fail
(some
estimates go up to 70 percent!).
By
following a plan preferably one
that is simple and brief
and
coming
up with design structure before
coding, you'll discover
that
things
fall together far more
easily than if you dive in and
start
hacking,
and you'll also realize a great
deal of satisfaction. It's my
experience
that coming up with an elegant
solution is deeply
satisfying
at an entirely different level; it
feels closer to art
than
technology.
And elegance always pays
off; it's not a
frivolous
pursuit.
Not only does it give you a program
that's easier to build
and
debug, but it's also easier
to understand and maintain, and
that's
where the financial value
lies.
Extreme
programming
I
have studied analysis and
design techniques, on and off,
since I
was
in graduate school. The
concept of Extreme
Programming (XP)
is
the
most radical, and delightful,
that I've seen. You can find
it
chronicled
in Extreme
Programming Explained by Kent
Beck
(Addison-Wesley
2000) and on the Web at
www.xprogramming.com.
XP
is both a philosophy about
programming work and a set of
guidelines
to do it. Some of these
guidelines are reflected in
other
recent
methodologies, but the two most
important and distinct
contributions,
in my opinion, are "write tests
first" and "pair
programming."
Although he argues strongly for the whole
process,
Beck
points out that if you adopt only
these two practices you'll
greatly
improve your productivity and
reliability.
Write
tests first
Testing
has traditionally been
relegated to the last part
of a project,
after
you've "gotten everything working, but
just to be sure." It's
implicitly
had a low priority, and people who
specialize in it have
not
been given a lot of status and
have often even been
cordoned
off
in a basement, away from the "real
programmers." Test
teams
66
Thinking
in C++
have
responded in kind, going so
far as to wear black
clothing and
cackling
with glee whenever they broke
something (to be
honest,
I've
had this feeling myself when
breaking C++ compilers).
XP
completely revolutionizes the
concept of testing by giving
it
equal
(or even greater) priority
than the code. In fact, you write
the
tests
before
you write
the code that's being
tested, and the tests
stay
with
the code forever. The
tests must be executed
successfully
every
time you do an integration of the
project (which is often,
sometimes
more than once a
day).
Writing
tests first has two
extremely important
effects.
First,
it forces a clear definition of
the interface of a class. I've
often
suggested
that people "imagine the
perfect class to solve
a
particular
problem" as a tool when trying to design
the system. The
XP
testing strategy goes further than
that it specifies exactly
what
the
class must look like, to the
consumer of that class, and
exactly
how
the class must behave. In no
uncertain terms. You can write
all
the
prose, or create all the
diagrams you want describing how a
class
should behave and what it looks
like, but nothing is as real
as
a
set of tests. The former is
a wish list, but the tests
are a contract
that
is enforced by the compiler and
the running program. It's hard
to
imagine a more concrete
description of a class than the
tests.
While
creating the tests, you are
forced to completely think out
the
class
and will often discover needed
functionality that might be
missed
during the thought experiments of UML
diagrams, CRC
cards,
use cases, etc.
The
second important effect of writing
the tests first comes
from
running
the tests every time you do
a build of your software. This
activity
gives you the other half of
the testing that's performed
by
the
compiler. If you look at the
evolution of programming
languages
from this perspective, you'll see
that the real
improvements
in the technology have
actually revolved
around
testing.
Assembly language checked only for
syntax, but C imposed
some
semantic restrictions, and these
prevented you from making
1:
Introduction to Objects
67
certain
types of mistakes. OOP
languages impose even
more
semantic
restrictions, which if you think about it
are actually forms
of
testing. "Is this data type
being used properly? Is this
function
being
called properly?" are the
kinds of tests that are
being
performed
by the compiler or run-time
system. We've seen
the
results
of having these tests built into
the language: people
have
been
able to write more complex
systems, and get them to work,
with
much less time and effort. I've
puzzled over why this is,
but
now
I realize it's the tests:
you do something wrong, and the
safety
net
of the built-in tests tells
you there's a problem and points you
to
where
it is.
But
the built-in testing
afforded by the design of
the language can
only
go so far. At some point,
you
must step
in and add the rest of
the
tests that produce a full
suite (in cooperation with the
compiler
and
run-time system) that
verifies all of your program. And,
just
like
having a compiler watching
over your shoulder, wouldn't you
want
these tests helping you right from
the beginning? That's
why
you
write them first, and run them automatically with
every build
of
your system. Your tests become an
extension of the safety
net
provided
by the language.
One
of the things that I've
discovered about the use of
more and
more
powerful programming languages is that I
am emboldened to
try
more brazen experiments,
because I know that the
language
will
keep me from wasting my time
chasing bugs. The XP
test
scheme
does the same thing for your
entire project. Because
you
know
your tests will always catch any
problems that you
introduce
(and
you regularly add any new tests as you
think of them), you
can
make big changes when you
need to without worrying that
you'll
throw the whole project into complete
disarray. This is
incredibly
powerful.
Pair
programming
Pair
programming goes against the
rugged individualism
that
we've
been indoctrinated into from the
beginning, through school
68
Thinking
in C++
(where
we succeed or fail on our own, and working with
our
neighbors
is considered "cheating") and media,
especially
Hollywood
movies in which the hero is usually
fighting against
of
individuality "cowboy coders" as Larry
Constantine likes to
say.
And yet XP, which is itself battling
against conventional
thinking,
says that code should be
written with two people per
workstation.
And that this should be done
in an area with a group
of
workstations, without the barriers
that the facilities
design
people
are so fond of. In fact,
Beck says that the
first task of
converting
to XP is to arrive with screwdrivers and Allen
wrenches
manager
who can deflect the ire of
the facilities
department.)
The
value of pair programming is
that one person is actually
doing
the
coding while the other is thinking
about it. The thinker
keeps
the
big picture in mind, not only the
picture of the problem at
hand,
but
the guidelines of XP. If two
people are working, it's
less likely
that
one of them will get away with saying, "I
don't want to write
the
tests first," for example. And if
the coder gets stuck, they
can
swap
places. If both of them get
stuck, their musings may
be
overheard
by someone else in the work
area who can
contribute.
Working
in pairs keeps things flowing and on
track. Probably more
important,
it makes programming a lot more
social and fun.
I've
begun using pair programming
during the exercise periods
in
some
of my seminars and it seems to
significantly improve
everyone's
experience.
16
Although this may be a
more American perspective,
the stories of
Hollywood
reach
everywhere.
17
Including (especially) the
PA system. I once worked in a
company that insisted
on
broadcasting
every phone call that
arrived for every executive,
and it constantly
interrupted
our productivity (but the
managers couldn't begin to
conceive of stifling
such
an important service as the
PA). Finally, when no one
was looking I started
snipping
speaker wires.
1:
Introduction to Objects
69
Why
C++ succeeds
Part
of the reason C++ has been
so successful is that the
goal was
not
just to turn C into an OOP language
(although it started
that
way),
but also to solve many other
problems facing
developers
today,
especially those who have
large investments in C.
Traditionally,
OOP languages have suffered
from the attitude
that
you
should abandon everything you know and
start from scratch
with
a new set of concepts and a new syntax,
arguing that it's
better
in
the long run to lose all the old
baggage that comes
with
procedural
languages. This may be true, in
the long run. But in the
short
run, a lot of that baggage was
valuable. The most
valuable
elements
may not be the existing code base
(which, given
adequate
tools,
could be translated), but instead
the existing mind
base. If
you're
a functioning C programmer and must drop
everything you
know
about C in order to adopt a new
language, you immediately
become
much less productive for many months,
until your mind
fits
around the new paradigm.
Whereas if you can leverage off
of
your
existing C knowledge and expand on
it, you can continue
to
be
productive with what you already know while moving
into the
world
of object-oriented programming. As
everyone has his or
her
own
mental model of programming,
this move is messy enough
as
it
is without the added expense of
starting with a new language
model
from square one. So the
reason for the success of
C++, in a
nutshell,
is economic: It still costs to move to
OOP, but C++ may
The
goal of C++ is improved productivity.
This productivity
comes
in
many ways, but the language is
designed to aid you as much as
possible,
while hindering you as little as possible
with arbitrary
rules
or any requirement that you use a
particular set of
features.
C++
is designed to be practical; C++ language
design decisions
18
I say "may" because, due to
the complexity of C++, it
might actually be cheaper
to
move
to Java. But the decision of
which language to choose has
many factors, and in
this
book I'll assume that
you've chosen C++.
70
Thinking
in C++
were
based on providing the maximum
benefits to the
programmer
(at
least, from the world view of
C).
A
better C
You
get an instant win even if you
continue to write C code
because
C++ has closed many holes in
the C language and
provides
better
type checking and compile-time analysis.
You're forced to
declare
functions so that the
compiler can check their
use. The need
for
the preprocessor has virtually
been eliminated for
value
substitution
and macros, which removes a set of
difficult-to-find
bugs.
C++ has a feature called
references
that
allows more
convenient
handling of addresses for function
arguments and
return
values. The handling of
names is improved through a
feature
called function
overloading, which
allows you to use the
same
name
for different functions. A feature
called namespaces
also
improves
the control of names. There
are numerous smaller
features
that improve the safety of
C.
You're
already on the learning curve
The
problem with learning a new language is
productivity. No
company
can afford to suddenly lose
a productive software
engineer
because he or she is learning a new
language. C++ is an
extension
to C, not a complete new syntax and
programming
model.
It allows you to continue creating
useful code, applying
the
features
gradually as you learn and understand
them. This may be
one
of the most important
reasons for the success of
C++.
In
addition, all of your existing C code is
still viable in C++,
but
because
the C++ compiler is pickier, you'll
often find hidden C
errors
when recompiling the code in
C++.
Efficiency
Sometimes
it is appropriate to trade execution
speed for
programmer
productivity. A financial model, for
example, may be
useful
for only a short period of time, so
it's more important
to
1:
Introduction to Objects
71
create
the model rapidly than to
execute it rapidly. However,
most
applications
require some degree of
efficiency, so C++ always
errs
on
the side of greater
efficiency. Because C programmers
tend to be
very
efficiency-conscious, this is also a way
to ensure that they
won't
be able to argue that the
language is too fat and
slow. A
number
of features in C++ are intended to allow
you to tune for
performance
when the generated code
isn't efficient
enough.
Not
only do you have the same
low-level control as in C (and
the
ability
to directly write assembly language
within a C++ program),
but
anecdotal evidence suggests
that the program speed for
an
object-oriented
C++ program tends to be within ±10% of a
program
OOP
program may actually be more
efficient than the C
counterpart.
Systems
are easier
to
express and understand
Classes
designed to fit the problem
tend to express it better.
This
means
that when you write the code,
you're describing your
solution
in the terms of the problem
space ("Put the grommet in
the
bin")
rather than the terms of the
computer, which is the
solution
space
("Set the bit in the
chip that means that
the relay will
close").
You
deal with higher-level concepts and
can do much more with a
single
line of code.
The
other benefit of this ease
of expression is maintenance,
which
(if
reports can be believed)
takes a huge portion of the
cost over a
program's
lifetime. If a program is easier to
understand, then it's
easier
to maintain. This can also
reduce the cost of creating
and
maintaining
the documentation.
19
However, look at Dan
Saks' columns in the
C/C++
User's Journal for
some
important
investigations into C++
library performance.
72
Thinking
in C++
Maximal
leverage with libraries
The
fastest way to create a program is to
use code that's
already
written:
a library. A major goal in C++ is to
make library use
easier.
This
is accomplished by casting libraries into
new data types
(classes),
so that bringing in a library
means adding new types
to
the
language. Because the C++
compiler takes care of how
the
library
is used guaranteeing proper
initialization and cleanup,
and
ensuring that functions are
called properly you can
focus on
what
you want the library to do, not how you
have to do it.
Because
names can be sequestered to
portions of your program via
C++
namespaces, you can use as many
libraries as you want
without
the kinds of name clashes
you'd run into with C.
Source-code
reuse with templates
There
is a significant class of types
that require
source-code
modification
in order to reuse them effectively.
The template
feature
in
C++ performs the source code
modification automatically,
making
it an especially powerful tool for
reusing library code.
A
type
that you design using
templates will work effortlessly
with
many
other types. Templates are
especially nice because they
hide
the
complexity of this kind of code
reuse from the client
programmer.
Error
handling
Error
handling in C is a notorious problem, and
one that is often
ignored
finger-crossing is usually involved. If you're
building a
large,
complex program, there's
nothing worse than having
an
error
buried somewhere with no clue as to
where it came from.
C++
exception
handling (introduced
in this Volume, and fully
covered
in Volume 2, which is downloadable from
) is a way to
guarantee that an error is
noticed
and
that something happens as a
result.
1:
Introduction to Objects
73
Programming
in the large
Many
traditional languages have
built-in limitations to
program
size
and complexity. BASIC, for example,
can be great for pulling
together
quick solutions for certain classes of
problems, but if the
program
gets more than a few pages long or
ventures out of the
normal
problem domain of that
language, it's like trying to
swim
through
an ever-more viscous fluid. C, too,
has these
limitations.
For
example, when a program gets
beyond perhaps 50,000 lines
of
code,
name collisions start to
become a problem effectively,
you
run
out of function and variable names.
Another particularly
bad
problem
is the little holes in the C
language errors buried in
a
large
program can be extremely
difficult to find.
There's
no clear line that tells you
when your language is failing
you,
and even if there were, you'd
ignore it. You don't say,
"My
BASIC
program just got too
big; I'll have to rewrite it in
C!"
Instead,
you try to shoehorn a few more lines in
to add that one
new
feature. So the extra costs
come creeping up on you.
C++
is designed to aid programming
in the large, that
is, to erase
those
creeping-complexity boundaries between a
small program
and
a large one. You certainly don't
need to use OOP,
templates,
namespaces,
and exception handling when you're writing a
hello-
world
style utility program, but those
features are there when
you
need
them. And the compiler is
aggressive about ferreting out
bug-
producing
errors for small and large
programs alike.
Strategies
for transition
If
you buy into OOP, your next question is
probably, "How can I
get
my manager/colleagues/department/peers to start
using
objects?"
Think about how you one independent
programmer
would
go about learning to use a new
language and a new
programming
paradigm. You've done it before.
First comes
education
and examples; then comes a trial
project to give you a
feel
for the basics without doing
anything too confusing.
Then
74
Thinking
in C++
comes
a "real world" project that
actually does something
useful.
Throughout
your first projects you continue your
education by
reading,
asking questions of experts, and
trading hints with
friends.
This
is the approach many experienced
programmers suggest for
the
switch from C to C++. Switching an
entire company will of
course
introduce certain group dynamics, but it
will help at each
step
to remember how one person would do
it.
Guidelines
Here
are some guidelines to
consider when making the
transition
to
OOP and C++:
1.
Training
The
first step is some form of
education. Remember the
company's
investment
in plain C code, and try not to throw
everything into
disarray
for six to nine months while
everyone puzzles over
how
multiple
inheritance works. Pick a
small group for indoctrination,
preferably
one composed of people who
are curious, work well
together,
and can function as their own
support network while
they're
learning C++.
An
alternative approach that is
sometimes suggested is
the
education
of all company levels at once,
including overview
courses
for strategic managers as well as design
and programming
courses
for project builders. This is
especially good for
smaller
companies
making fundamental shifts in
the way they do things, or
at
the division level of larger
companies. Because the cost
is higher,
however,
some may choose to start with
project-level training, do a
pilot
project (possibly with an outside
mentor), and let the
project
team
become the teachers for the
rest of the company.
2.
Low-risk project
Try
a low-risk project first and allow for
mistakes. Once you've
gained
some experience, you can
either seed other projects
from
members
of this first team or use
the team members as an
OOP
technical
support staff. This first
project may not work right the
first
time, so it should not be
mission-critical for the company.
It
1:
Introduction to Objects
75
should
be simple, self-contained, and
instructive; this means that
it
should
involve creating classes that will be
meaningful to the
other
programmers
in the company when they get
their turn to learn
C++.
3.
Model from success
Seek
out examples of good object-oriented
design before
starting
from
scratch. There's a good
probability that someone has
solved
your
problem already, and if they haven't
solved it exactly you
can
probably
apply what you've learned about
abstraction to modify an
existing
design to fit your needs. This is
the general concept
of
design
patterns, covered in
Volume 2.
4.
Use existing class libraries
The
primary economic motivation for switching
to OOP is the easy
use
of existing code in the form of
class libraries (in particular,
the
Standard
C++ libraries, which are covered in
depth in Volume two
of
this book). The shortest
application development cycle will
result
when
you don't have to write anything but main(
),
creating and
using
objects from off-the-shelf libraries.
However, some new
programmers
don't understand this, are
unaware of existing
class
libraries,
or, through fascination with the
language, desire to write
classes
that may already exist. Your
success with OOP and C++
will
be optimized if you make an effort to
seek out and reuse
other
people's
code early in the transition
process.
5.
Don't rewrite existing code in C++
Although
compiling
your C
code with a C++ compiler usually
produces
(sometimes tremendous) benefits by
finding problems in
the
old code, it is not usually the best
use of your time to
take
existing,
functional code and rewrite it in
C++. (If you must turn it
into
objects, you can "wrap" the C
code in C++ classes.) There
are
incremental
benefits, especially if the
code is slated for reuse.
But
chances
are you aren't going to see
the dramatic increases
in
productivity
that you hope for in your first few
projects unless that
project
is a new one. C++ and OOP shine
best when taking a
project
from
concept to reality.
76
Thinking
in C++
Management
obstacles
If
you're a manager, your job is to
acquire resources for your
team,
to
overcome barriers to your team's
success, and in general to try to
provide
the most productive and
enjoyable environment so your
team
is most likely to perform
those miracles that are
always being
asked
of you. Moving to C++ falls in all three of
these categories,
and
it would be wonderful if it didn't cost you anything
as well.
Although
moving to C++ may be cheaper depending on
your
programmers
(and probably for programmers in
other procedural
languages),
it isn't free, and there are
obstacles you should be
aware
of before trying to sell the move to C++
within your
company
and embarking on the move
itself.
Startup
costs
The
cost of moving to C++ is more than just
the acquisition of C++
compilers
(the GNU C++ compiler, one of
the very best, is
free).
Your
medium- and long-term costs will be
minimized if you invest
in
training (and possibly
mentoring for your first project) and
also
if
you identify and purchase class
libraries that solve your
problem
rather
than trying to build those libraries
yourself. These are
hard-
money
costs that must be factored into a
realistic proposal. In
addition,
there are the hidden
costs in loss of productivity
while
learning
a new language and possibly a new
programming
environment.
Training and mentoring can
certainly minimize
these,
but
team members must overcome
their own struggles to
understand
the new technology. During this
process they will
make
more mistakes (this is a
feature, because
acknowledged
mistakes
are the fastest path to
learning) and be less
productive.
Even
then, with some types of
programming problems, the
right
classes,
and the right development environment,
it's possible to be
more
productive while you're learning C++
(even considering
that
20
Because of its
productivity improvements, the
Java language should also
be
considered
here.
1:
Introduction to Objects
77
you're
making more mistakes and writing
fewer lines of code
per
day)
than if you'd stayed with C.
Performance
issues
A
common question is, "Doesn't
OOP automatically make
my
programs
a lot bigger and slower?" The
answer is, "It
depends."
Most
traditional OOP languages
were designed with
experimentation
and rapid prototyping in mind rather than
lean-
and-mean
operation. Thus, they virtually
guaranteed a significant
increase
in size and decrease in speed.
C++, however, is
designed
with
production programming in mind. When your
focus is on
rapid
prototyping, you can throw together
components as fast as
possible
while ignoring efficiency issues. If
you're using any third
party
libraries, these are usually
already optimized by
their
vendors;
in any case it's not an issue while
you're in rapid-
development
mode. When you have a system
that you like, if
it's
small
and fast enough, then you're
done. If not, you begin
tuning
with
a profiling tool, looking
first for speedups that can
be done
with
simple applications of built-in C++
features. If that
doesn't
help,
you look for modifications that
can be made in the
underlying
implementation
so no code that uses a
particular class needs to
be
changed.
Only if nothing else solves
the problem do you need
to
change
the design. The fact
that performance is so critical in
that
portion
of the design is an indicator
that it must be part of
the
primary
design criteria. You have
the benefit of finding this
out
early
using rapid
development.
As
mentioned earlier, the number
that is most often given for
the
difference
in size and speed between C and C++ is
±10%, and often
much
closer to par. You might even
get a significant
improvement
in
size and speed when using C++
rather than C because the
design
you
make for C++ could be quite
different from the one
you'd
make
for C.
The
evidence for size and speed
comparisons between C and C++
tends
to be anecdotal and is likely to remain
so. Regardless of the
number
of people who suggest that a
company try the same
project
78
Thinking
in C++
using
C and C++, no company is likely to
waste money that way
unless
it's very big and interested in
such research projects.
Even
then,
it seems like the money
could be better spent.
Almost
universally,
programmers who have moved from C (or
some other
procedural
language) to C++ (or some
other OOP language)
have
had
the personal experience of a
great acceleration in
their
programming
productivity, and that's the
most compelling
argument
you can find.
Common
design errors
When
starting your team into OOP and
C++, programmers will
typically
go through a series of common design
errors. This often
happens
because of too little
feedback from experts during
the
design
and implementation of early projects,
because no experts
have
been developed within the
company and there may be
resistance
to retaining consultants. It's easy to
feel that you
understand
OOP too early in the
cycle and go off on a bad
tangent.
Something
that's obvious to someone
experienced with the
language
may be a subject of great internal
debate for a novice.
Much
of this trauma can be
skipped by using an
experienced
outside
expert for training and
mentoring.
On
the other hand, the
fact that it is easy to make
these design
errors
points to C++'s main drawback:
its backward
compatibility
with
C (of course, that's also
its main strength). To accomplish
the
feat
of being able to compile C
code, the language had to
make
some
compromises, which have resulted in a
number of "dark
corners."
These are a reality, and
comprise much of the
learning
curve
for the language. In this
book and the subsequent
volume
(and
in other books; see Appendix
C), I try to reveal most of
the
pitfalls
you are likely to encounter when working
with C++. You
should
always be aware that there
are some holes in the
safety net.
Summary
This
chapter attempts to give you a
feel for the broad issues
of
object-oriented
programming and C++, including why
OOP is
1:
Introduction to Objects
79
different,
and why C++ in particular is different,
concepts of OOP
methodologies,
and finally the kinds of issues you will
encounter
when
moving your own company to OOP and
C++.
OOP
and C++ may not be for everyone. It's important to
evaluate
your
own needs and decide whether C++ will
optimally satisfy
those
needs, or if you might be better off with
another
programming
system (including the one
you're currently using). If
you
know that your needs will be very
specialized for the
foreseeable
future and if you have specific
constraints that may not
be
satisfied by C++, then you owe it to
yourself to investigate
the
you'll
at least understand what the
options were and have a
clear
vision
of why you took that direction.
You
know what a procedural program looks
like: data
definitions
and
function calls. To find the
meaning of such a program you
have
to
work a little, looking through the
function calls and
low-level
concepts
to create a model in your mind. This is
the reason we need
intermediate
representations when designing procedural
programs
by themselves, these programs
tend to be confusing because
the
terms
of expression are oriented
more toward the computer than
to
the
problem you're solving.
Because
C++ adds many new concepts to the C
language, your
natural
assumption may be that the
main(
) in
a C++ program will
be
far more complicated than for
the equivalent C program.
Here,
you'll
be pleasantly surprised: A well-written
C++ program is
generally
far simpler and much easier to
understand than the
equivalent
C program. What you'll see are
the definitions of
the
objects
that represent concepts in your
problem space (rather
than
the
issues of the computer
representation) and messages sent
to
those
objects to represent the
activities in that space. One of
the
21
In particular, I recommend
looking at Java (http://java.sun.com)
and Python
(http://www.Python.org).
80
Thinking
in C++
delights
of object-oriented programming is that,
with a well-
designed
program, it's easy to
understand the code by
reading it.
Usually
there's a lot less code, as well,
because many of your
problems
will be solved by reusing existing
library code.
1:
Introduction to Objects
81
Table of Contents:
|
|||||