How to Be a Better Coder

Coding is not mechanical. If it were, all the CASE tools that people pinned their hopes on in the early 1980s would have replaced programmers long ago. There are decisions to be made every minute—decisions that require careful thought and judgment if the resulting program is to enjoy a long, accurate, and productive life.

This chapter is from the book

This chapter is from the book

Conventional wisdom says that once a project is
in the coding phase, the work is mostly mechanical, transcribing the design
into executable statements. We think that this attitude is the single biggest
reason that many programs are ugly, inefficient, poorly structured,
unmaintainable, and just plain wrong.

Coding is not mechanical. If it were, all the
CASE tools that people pinned their hopes on in the early 1980s would have
replaced programmers long ago. There are decisions to be made every
minute—decisions that require careful thought and judgment if the
resulting program is to enjoy a long, accurate, and productive life.

Developers who don't actively think about their
code are programming by coincidence—the code might work, but there's no
particular reason why. In Programming by
Coincidence, we advocate a more positive involvement with the coding
process.

While most of the code we write executes quickly,
we occasionally develop algorithms that have the potential to bog down even
the fastest processors. In Algorithm Speed,
we discuss ways to estimate the speed of code, and we give some tips on how
to spot potential problems before they happen.

Pragmatic Programmers think critically about all
code, including our own. We constantly see room for improvement in our
programs and our designs. In Refactoring, we
look at techniques that help us fix up existing code even while we're in the
midst of a project.

Something that should be in the back of your mind
whenever you're producing code is that you'll someday have to test it. Make
code easy to test, and you'll increase the likelihood that it will actually
get tested, a thought we develop in Code That's
Easy to Test.

Finally, in Evil
Wizards, we suggest that you should be careful of tools that write
reams of code on your behalf unless you understand what they're doing.

Most of us can drive a car largely on
autopilot—we don't explicitly command our foot to press a pedal, or our
arm to turn the wheel—we just think "slow down and turn right."
However, good, safe drivers are constantly reviewing the situation, checking
for potential problems, and putting themselves into good positions in case
the unexpected happens. The same is true of coding—it may be largely
routine, but keeping your wits about you could well prevent a disaster.

Programming by Coincidence

Do you ever watch old black-and-white war movies?
The weary soldier advances cautiously out of the brush. There's a clearing
ahead: are there any land mines, or is it safe to cross? There aren't any
indications that it's a minefield—no signs, barbed wire, or craters.
The soldier pokes the ground ahead of him with his bayonet and winces,
expecting an explosion. There isn't one. So he proceeds painstakingly through
the field for a while, prodding and poking as he goes. Eventually, convinced
that the field is safe, he straightens up and marches proudly forward, only
to be blown to pieces.

The soldier's initial probes for mines revealed
nothing, but this was merely lucky. He was led to a false
conclusion—with disastrous results.

As developers, we also work in minefields. There
are hundreds of traps just waiting to catch us each day. Remembering the
soldier's tale, we should be wary of drawing false conclusions. We should
avoid programming by coincidence—relying on luck and accidental
successes— in favor of programming
deliberately.

How to Program by Coincidence

Suppose Fred is given a programming
assignment. Fred types in some code, tries it, and it seems to work.
Fred types in some more code, tries it, and it still seems to work.
After several weeks of coding this way, the program suddenly stops
working, and after hours of trying to fix it, he still doesn't know
why. Fred may well spend a significant amount of time chasing this
piece of code around without ever being able to fix it. No matter
what he does, it just doesn't ever seem to work right.

Fred doesn't know why the code is failing
because he didn't know why it worked in the
first place. It seemed to work, given the limited "testing"
that Fred did, but that was just a coincidence. Buoyed by false
confidence, Fred charged ahead into oblivion. Now, most intelligent
people may know someone like Fred, but
we know better. We don't rely on coincidences—do we?

Sometimes we might. Sometimes it can be
pretty easy to confuse a happy coincidence with a purposeful plan.
Let's look at a few examples.

Accidents of Implementation

Accidents of implementation are
things that happen simply because that's the way the code is
currently written. You end up relying on undocumented error
or boundary conditions.

Suppose you call a routine with
bad data. The routine responds in a particular way, and you
code based on that response. But the author didn't intend for
the routine to work that way—it was never even
considered. When the routine gets "fixed," your code may
break. In the most extreme case, the routine you called may
not even be designed to do what you want, but it seems to work okay. Calling things
in the wrong order, or in the wrong context, is a related
problem.

Here it looks like Fred is
desperately trying to get something out on the screen. But
these routines were never designed to be called this way;
although they seem to work, that's really just a
coincidence.

To add insult to injury, when the
component finally does get drawn, Fred won't try to go back
and take out the spurious calls. "It works now, better leave
well enough alone…."

It's easy to be fooled by this
line of thought. Why should you take the risk of messing with
something that's working? Well, we can think of several
reasons:

It may not really be working—it might just look like it is.

The boundary condition you rely on may be just an accident. In different
circumstances (a different screen resolution, perhaps), it might behave differently.

Undocumented behavior may change with the next release of the library.

Additional and unnecessary calls make your code slower.

Additional calls also increase the risk of introducing new bugs of their
own.

For code you write that others
will call, the basic principles of good modularization and of
hiding implementation behind small, well-documented
interfaces can all help. A well-specified contract (see Design by Contract)
can help eliminate misunderstandings.

For routines you call, rely only
on documented behavior. If you can't, for whatever reason,
then document your assumption well.

Accidents of Context

You can have "accidents of
context" as well. Suppose you are writing a utility module.
Just because you are currently coding for a GUI environment,
does the module have to rely on a GUI being present? Are you
relying on English-speaking users? Literate users? What else
are you relying on that isn't guaranteed?

Implicit Assumptions

Coincidences can mislead at all
levels—from generating requirements through to testing.
Testing is particularly fraught with false causalities and
coincidental outcomes. It's easy to assume that X causes Y, but as we said in Debugging: don't
assume it, prove it.

At all levels, people operate
with many assumptions in mind—but these assumptions are
rarely documented and are often in conflict between different
developers. Assumptions that aren't based on well-established
facts are the bane of all projects.

Tip 44

Don't Program by Coincidence

How to Program Deliberately

We want to spend less time churning out
code, catch and fix errors as early in the development cycle as
possible, and create fewer errors to begin with. It helps if we can
program deliberately:

Always be aware of what you are doing. Fred let things get slowly out of hand,
until he ended up boiled, like the frog in Stone Soup and
Boiled Frogs.

Don't code blindfolded. Attempting to build an application you don't fully
understand, or to use a technology you aren't familiar with, is an invitation to be misled by
coincidences.

Proceed from a plan, whether that plan is in your head, on the back of a
cocktail napkin, or on a wall-sized printout from a CASE tool.

Rely only on reliable things. Don't depend on accidents or assumptions. If you
can't tell the difference in particular circumstances, assume the worst.

Document your assumptions. Design by
Contract, can help clarify your assumptions in your own mind, as well as help communicate them to
others.

Don't just test your code, but test your assumptions as well. Don't guess;
actually try it. Write an assertion to test your assumptions (see Assertive Programming). If your assertion is right, you have improved the
documentation in your code. If you discover your assumption is wrong, then count yourself lucky.

Prioritize your effort. Spend time on the important aspects; more than likely,
these are the hard parts. If you don't have fundamentals or infrastructure correct, brilliant bells
and whistles will be irrelevant.

Don't be a slave to history. Don't let existing code dictate future code. All
code can be replaced if it is no longer appropriate. Even within one program, don't let what you've
already done constrain what you do next—be ready to refactor (see Refactoring). This decision may impact the project schedule. The assumption
is that the impact will be less than the cost of not making the
change.1

So next time something seems to work, but
you don't know why, make sure it isn't just a coincidence.

This code comes from a general- purpose Java tracing
suite. The function writes a string to a log file. It passes its unit
test, but fails when one of the Web developers uses it. What coincidence
does it rely on?