Inapoi
Inainte
Cuprins
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 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 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 (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 mindless
conformity[19].
Programmers, too, are considered paragons 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 and take apart everything that gets in the
way.[20] (This will
require a 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.
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 cost
less[21].
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 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
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 written in
C, and often much
closer[22].
The design produced for an 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.
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 www.BruceEckel.com) is a
way to guarantee that an error is noticed and that something happens as a
result.
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 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 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.
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
constraints[23]
– than the OOP alternatives for a team of C 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 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 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 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
alternatives[24].
Even if you eventually choose C++ as your language, 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 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.
2: Making & Using Objects
[4]
See Multiparadigm Programming in Leda by Timothy Budd (Addison-Wesley
1995).
[5]
You can find an interesting implementation of this problem in Volume 2 of this
book, available at www.BruceEckel.com.
[6]
Some people make a distinction, stating that type determines the interface while
class is a particular implementation of that interface.
[7]
I’m indebted to my friend Scott Meyers for this term.
[8]
This is usually enough detail for most diagrams, and you don’t need to get
specific about whether you’re using aggregation or
composition.
[9]
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.
[10]
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.
[11]
Thanks for help from James H Jarrett.
[12]
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).
[13]
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.
[14]
For starters, I recommend the aforementioned UML
Distilled.
[15]
Python (www.Python.org) is often used as “executable
pseudocode.”
[16]
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.
[17]
This term is explored in the Design Patterns chapter in Volume
2.
[18]
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.
[19]
Although this may be a more American perspective, the stories of Hollywood reach
everywhere.
[20]
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.
[21]
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++.
[22]
However, look at Dan Saks’ columns in the C/C++ User’s
Journal for some important investigations into C++ library
performance.
[23]
Because of its productivity improvements, the Java language should also be
considered here.
[24]
In particular, I recommend looking at Java (http://java.sun.com) and Python
(http://www.Python.org).
 |
|