How to Write Excellent Code
Topic Category
Software coding best practicesReading Sections
Introduction
Principles for the Development of a Complete Mind:
Study the science of art. Study the art of science.
Develop your senses—especially learn how to see.
Realize that everything connects to everything else.
Study the science of art. Study the art of science.
Develop your senses—especially learn how to see.
Realize that everything connects to everything else.
The point of these questions is simply to get you thinking, to make you delve deeper into each subject, and to spur you to improve your programming skills.
If you’re thinking of reading this just to get the “answers” without having thought about the questions first, I’d really encourage you not to. Spending even a little time mulling things over and getting personal will really pay off.
As Confucius said,
“I hear and I forget.
I see and I remember.
I do and I understand.”
I see and I remember.
I do and I understand.”
1. On the Defensive
Mull It Over
1. Can you have too much defensive programming?
Yes—just as too many comments can degrade code readability, so can many defensive checks, if they are bad. Redundant checks can be avoided with careful coding; for example, by making a good choice of types.
2. Should you add an assertion to your code for every bug you find and fix?
Fundamentally, it’s not a bad practice. But think about where you’d add the assertions. Many, many faults are due to incorrect honoring of API contracts. If you passed garbage into a function, you would want to put some precondition checking inside that function, rather than put a test at the call site. If the function returned garbage, you would either fix the function so that it won’t again (and prove it’s fixed) or write some postconditions.
It would be more beneficial to add a new unit test for every bug you find and fix.
3. Should assertions conditionally compile away to nothing in production builds? If not, which assertions should remain in release builds?
People hold passionate beliefs on this subject. The answer isn’t black and white; there are powerful arguments for both sides. There are always some very nit-picky assertions that really don’t need to be left in production builds. But some assertion occurrences may still interest you in the field.
Now, if you do leave any constraint checks in releases, they must change behavior—the program shouldn’t abort on failure, just log the problem and move on.
Remember: Genuine run-time error checks should never be removed; they should never be coded in assertions anyway.
4. Are exceptions a better form of defensive barrier than C-style assertions?
They can be. Exceptions behave differently; while propagating back up the call stack, an exception can be caught and ignored—suppressing its effect. This makes exceptions more flexible tools. You can’t ignore an assert that aborts execution; assertions are lower-level mechanisms.
5. Should the defensive checking of pre- and postconditions be put inside each function, or around each important function call?
In the function, without a doubt. This way, you only need to write tests once. The only reason you’d want to move them out is to gain flexibility, to choose what happens when a constraint fails. This isn’t a compelling gain for such an explosion in complexity and potential for failure.
6. Are constraints a perfect defensive tool? What are their drawbacks?
No, they are nowhere near perfect. Redundant constraints can be pests at best and hindrances at worst. For example, you could assert that a function parameter i >= 0. But it’s much better to make i an unsigned type that can’t contain invalid values anyway.
Treat constraints that can be compiled out with a certain degree of suspicion: We must carefully check for any side effects (assertions can have subtle indirect consequences) and for timing issues in the debug build that alters its behavior from a release build. Ensure that assertions are logical constraints and not genuine run-time checks that mustn't be compiled out. It is possible to put bugs in the bug-defense code!
But carefully used, constraints are still far better than dancing barefoot over the hot coals of chance.
7. Can you avoid defensive programming?
a. If you designed a better language, would defensive programming still be necessary? How could you do this?
b. Does this show that C and C++ are flawed because they have so many areas for problems to manifest?
Some language features certainly could be designed to avoid errors. For example, C doesn’t check the index of any array lookup you perform. As a result, you can crash the program by accessing an invalid memory address. The Java run time, on the other hand, checks every array index before lookup, so such an catastrophe will never arise. (Bad indexes will still cause an error though, just a better defined class of failure.)
Despite the long list of “improvements” you could make to the liberal C specification (and I urge you to think of as many as you can), you’ll never be able to create a language that doesn’t need defensive programming. Functions will always need to validate parameters, and classes will always need invariants to check that their data is internally consistent.
Although C and C++ do provide plenty of opportunity for things to go wrong, they also provide a great deal of power and expression. Whether that makes the languages flawed depends on your viewpoint—this is a topic ripe for holy war.
8. What sort of code do you not need to worry about writing defensively?
I’ve worked with people who refused to put any defensive code into an old program because it was so bad that their defenses would make no difference. I managed to resist the urge to whack them with a large mallet.
You might argue that a small, stand-alone, single-file program or a test harness doesn’t need this sort of careful defensive code or any rigorous constraints. But even in these situations, not being careful is just being sloppy. We should aim to be defensive all the time.
Getting Personal
1. How carefully do you consider each statement that you type? Do you relentlessly check every function return code, even if you’re sure a function will not return an error?
I bet you don’t check everything. It’s far too easy to overlook certain function return codes, especially since some are deemed more important than others. How many C programmers check the return value of printf? How many actually know that it returns anything?
2. When you document a function, do you state the pre- and postconditions?
a. Are they always implicit in the description of what the function does?
b. If there are no pre- or postconditions, do you explicitly document this?
No matter how obvious you think a contract is (from the function name or its description), explicitly stating the constraints removes any ambiguity— remember, it’s always better to remove areas of assumption. Explicitly writing Preconditions: None will document a contract explicitly.
Of course, you don’t want every function to explicitly restate a global
precondition. It would be laborious and tedious. If an entire API expects
that pointer values mustn’t be null, it’s arguably better to document this
once, globally.
3. Many companies pay lip service to defensive programming. Does your team recommend it? Take a look at the codebase—do they really? How widely are constraints codified in assertions? How thorough is the error checking in each function?
Very few companies have a culture of excellent code with the right level of defense. Code reviews are a good way to bring a team’s code up to a reasonable standard; many eyes see many more potential errors.
4. Are you naturally paranoid enough? Do you look both ways before crossing the road? Do you eat your greens? Do you check for every potential error in your code, no matter how unlikely?
a. How easy is it to do this thoroughly? Do you forget to think about errors?
b. Are there any ways to help yourself write more thorough defensive code?
No one finds it naturally easy—thinking the worst of your carefully crafted new code is contrary to a programmer’s instincts. Instead, expect the worst of any people who will be using your code. They’re nowhere near as conscientious a programmer as you are!
A very helpful technique is to write unit tests for each function or class.
Some experts strongly advise doing this before writing a function, which makes
a lot of sense. It helps you to think about all the error cases, rather than
happily trusting that your code will work.
2. The Best Laid Plans
Mull It Over
1. Should you alter the layout of legacy code to conform to your latest code style? Is this a valuable use of code reformatting tools?
It’s usually safest to leave legacy code however you find it, even if it’s ugly and hard to work with. I’d only entertain reformatting if I was absolutely sure that none of the original authors would ever need to return.
By reformatting, you lose the ability to easily compare a particular revision of the source with a previous one—you’ll be thrown by many, many formatting changes which may hide the one important difference you really need to see. You also risk introducing program errors in the reformatting.
As far as code reformatting tools go, they’re nice curiosities, but I don’t advocate the use of them. Some companies insist on running source files through beautifiers before checking any code into their repository. The advantage is that all code is homogenized, pasteurized, and uniformly formatted. The major disadvantage is that no tool is perfect; you’ll lose some helpful nuances of the author’s layout. Unless all the programmers on your team are gibbons, don’t use a reformatting tool.
2. A common layout convention is to split source lines at a set number of columns. What are the pros and cons of this? Is it useful?
As with many presentation concerns, there is no absolute answer; it is a matter of personal taste.
I like to split my code up so that it fits on an 80-column display. I’ve always done that, so it’s a matter of habit as much as anything else. I don’t disagree with people who like long lines, but I find long lines hard to work with. I set my editor up to wrap continuous lines rather than provide a horizontal scrollbar (horizontal scrolling is clumsy). In this environment, long lines tend to ruin the effect of any indentation.
As I see it, the main advantage of fixed column widths is not printability, as some would claim. It’s the ability to have several editor windows open side by side on the same display.
In practice, C++ produces very long lines. It’s more verbose than C; you end up calling member functions on objects referenced by another object through a templated container. . . . There are strategies to manage the many, many, long lines this may lead to. You can store intermediate references in temporary variables, for example.
3. How detailed should a reasonable coding standard be?
a. How serious are deviations from the style? How many limbs should be amputated for not following it?
b. Can a standard become too detailed and restrictive? What would happen if it did?
Six limbs should be amputated for deviations from any coding standard.
The correct answer really depends on the exhaustiveness of the coding standard and the coding culture you work in. There are usually much bigger software problems to address than a misplaced bracket, but brackets are easier to moan about. I have seen many coding standards that are so prescriptive and paralyzing that the poor programmers have just plain ignored them. To be useful and to be accepted, a coding standard should provide a little room for maneuvering, perhaps with a best practice approach given as an example.
4. When defining a new presentation st


