Program Defensively
From Programmer 97-things
It pays to be defensive when programming. Defensiveness means writing code that is good at catching and identifying bugs during development, as well as being resilient to bad input in production.
In some cases compilers, languages, and other tools help avoid these issues at compile time, but there are many ways in which we can help by programming defensively. Here are just a few:
- Initialize variables when they are declared to avoid surprises.
- Watch out for truncation on division and numeric overflows in integer expressions. One way to do this is to use assertions to recompute initial values from results and make sure they agree.
- Be careful when using floating-point arithmetic: avoid expressions with numbers of vastly different magnitudes, and don't use exact equality comparisons for values that are the result of computations (as opposed to constant assignments); instead use range checks.
- Check denominators before dividing to catch divide-by-zero issues.
- Avoid mixed type comparisons: eliminate the cause of the mixed types at its source if possible, and use explicit casts otherwise.
- Pay attention to operator precedence and order of evaluation: use parentheses to ensure behavior.
- Check the return value of functions that return error codes, and avoid creating such functions yourself if you have the option of using exceptions instead.
- In languages that don't do bounds checking, add an extra element to arrays with a known value. Check that value later to catch off-by-one (fencepost) write errors.
- Check pointers before dereferencing them.
- Eliminate compiler warnings so that real compiler signals don't get lost in noise.
- Initialize dynamic memory and use this to detect unexpected writes.
- Set pointers to null after they are no longer valid.
- Use a reserve "parachute" of memory. If your program runs out of memory, release the parachute before shutting down to help ensure you have the resources to shut down gracefully.
- Give the initial element of enumerations a non-zero value to catch uninitialized enumeration variables.
- Override
toStringor add other dump methods to classes to help with debugging (many debuggers will let you call these methods during debugging).
- Use version numbers in protocols, file formats, etc., to check for compatibility.
- Use interfaces to reduce dependencies and help isolate problems.
- After fixing a bug, add a regression test or, if not possible, add an assertion to help catch regressions.
- Use assertions to test invariants. Sometimes you will have comments like "We know that..." — don't assume, use an assertion.
-
switchstatements with nodefaultcase could have assertions for the default case.
- Use assertions in places that should be unreachable.
- Use preconditions on non-public methods to enforce calling contracts — public methods should throw invalid argument exceptions — and use postconditions to verify functionality. Testers should try to pass the precondition but fail the postcondition.
- Use assertions to enforce early development simplifying assumptions, as well as longer lived design assumptions, such as those about the ranges of parameter values, to help catch future changes that violate the assumptions.
- To satisfy the Liskov Substitution Principle, preconditions of subclass overrides should be no stronger than those of the superclass and postconditions no weaker.
As you adopt a more defensive mindset you should find it pays great dividends in reducing the amount of time you have to spend debugging later; take pleasure in depriving testers of the joy of breaking your code!
This work is licensed under a Creative Commons Attribution 3
