I just love Steve McConnell’s classic book Code Complete 2, and I recommend it to everyone in the Software ‘world’ who’s willing to progress and sharpen his skills.
Other blog posts in this series:
Boolean Expressions
Except for the simplest control structure, the one that calls for the execution of statements in sequence, all control structures depend on the evaluation of boolean expressions.
Using true and false for Boolean Tests
Use the identifiers true
and false
in boolean expressions rather than using values like 0
and 1
.
You can write clearer tests by treating the expressions as boolean expressions. For example, write
while ( !done ) ...
while ( a > b ) ...
rather than
while ( done == false ) ...
while ( (a > b) == true ) ...
Using implicit comparisons reduces the numbers of terms that someone reading your code has to keep in mind, and the resulting expressions read more like conversational English.
Making Complicated Expressions Simple
Break complicated tests into partial tests with new boolean variables rather than creating a monstrous test with half a dozen terms, assign intermediate values to terms that allow you to perform a simpler test.
Move complicated expressions into boolean functions if a test is repeated often or distracts from the main flow of the program. For example:
If ( ( document.AtEndOfStream ) And ( Not inputError ) ) And
( ( MIN_LLINES <= lineCount ) And ( lineCount <= MAX_LINES ) ) And
( Not ErrorProcessing() ) Then
...
End If
This is an ugly test to have to read through if you’re not interested in the test itself. By putting it into a function, you can isolate the test and allow the reader to forget about it unless it’s important.
If ( DocumentIsValid( document , lineCount, inputError) ) Then
' do something other
...
End If
If you use the test only once, you might not think it’s worthwhile to put it into a routine. But putting the test into a well-named function improves readability and makes it easier for you to see what your code is doing, and that’s sufficient reason to do it.
Forming Boolean Expressions Positively
I ain’t not no undummy.
~ Homer Simpson
Not a few people don’t have not any trouble understanding a nonshort string of nonpositives-that is, most people have trouble understanding a lot of negatives.
You can do several things to avoid complicated negative boolean expressions in your program:
- In
if
statements, convert negatives to positives and flip-flop the code in the if
and else
clauses
- You shouldn’t write
if ( !statusOK )
. Alternatively, you could choose a different variable name, one that would reverse the truth value of the test. In the example, you could replace statusOK
with ErrorDetected
, which would be true when statusOK
was false.
- Apply DeMorgean’s Theorems to simplify boolean test with negatives
- DeMorgan’s Theorem lets you exploit the logical relationship between an expression and version of the expression that means the same thing because it’s doubly negated. For example:
if ( !displayOK || !printerOK ) ...
This is logically equvalent ot the following:
if ( !( displayOK && printerOK ) ) ...
Using Parentheses to Clarify Boolean Expressions
Using parentheses isn’t like sending a telegram: you’re not charged for each character – the extra characters are free.
Knowing How Boolean Expressions Are Evaluated
The pseudocodeThe pseudocode example of when “short-circuit” or “lazy” evaluation is necessary:
while( i < MAX_ELEMENTS and item[ i ] <> 0 ) ...
If the whole expression is evaluated, you’ll get an error on the last pass through the loop. The variable i
equals MAX_ELEMENTS
, so the expression item[ i ]
is an array-index error.
In the pseudocode, you could restructure the test so that the error doesn’t occur:
while ( i < MAX_ELEMENTS )
if (item[ i ] <> 0 ) then
...
C++ uses short-circuit evaluation: if the first operand of the and is false, the second isn’t evaluated because the whole expression would be false anyway.
Write Numeric Expressions in Number-Line Order
MIN_ELEMENTS <= i and i <= MAX_ELEMENTS
The idea is to order the elements left to right, from smallest to largest. In the first line, MIN_ELEMENTS
and MAX_ELEMENTS
are the two endpoints, so they go at the ends. The variable i
is supposed to be between them, so it goes in the middle.
Compound Statements (Blocks)
A “compound statement” or “block” is a collection of statements that are treated as a single statement for purposes of controlling the flow of a program. Compound statements are created by writing {
and }
around a group of statements in C++, C#, C, and Java.
⚠️ Use the block to clarify your intentions regardless of whether the code inside the block is 1 line or 20.
Taming Dangerously Deep Nesting
Excessive indentation, or “nesting,” has been pilloried in computing literature for 25 years and is still one of the chief culprits in confusing code.
Many researchers recommend avoiding nesting to more than three or four levels (Myers 1976, Marca 1981, and Ledgard and Tauer 1987a). Deep nesting works against what Chapter 5 describes as Software’s Major Technical Imperative: Managing Complexity. That is reason enough to avoid deep nesting.
It’s not hard to avoid deep nesting. If you have deep nesting, you can redesign the tests performed in the if
and else
clauses or you can break the code into simpler routines. Here are the tips to reduce the nesting depth:
Simplify the nested if
by testing part of the condition
if ( inputStatus == InputStatus_Success ) {
// lots of code
...
if ( printerRoutine != NULL ) {
// lots of code
...
if ( SetupPage() ) {
// lots of code
...
if ( AllocMem( &printData ) ) {
// lots of code
...
}
}
}
Here’s the code revised to use retesting rather than nesting:
if ( inputStatus == InputStatus_Success ) {
// lots of code
...
if ( printerRoutine != NULL ) {
// lots of code
... }
}
if ( ( inputStatus == InputStatus_Success ) &&
( printerRoutine != NULL ) && SetupPage() ) {
// lots of code
...
if ( AllocMem( &printData ) ) {
// lots of code
...
}
}
This is a particularly realistic example because it shows that you can’t reduce the nesting level for free; you have to put up with a more complicated test in return for the reduced level of nesting.
Simplify a nested if
by using break
If some condition in the middle of the block fails, execution continues at the end of the block.
do {
// begin break block
if ( inputStatus != InputStatus_Success ) {
break; // break out of block
}
// lots of code
...
if ( printerRoutine == NULL ) {
break; // break out of block
}
// lots of code
...
if ( !SetupPage() ) {
break; // break out of block
}
// lots of code
...
if ( !AllocMem( &printData ) ) {
break; // break out of block
}
// lots of code
...
} while (FALSE); // end break block
This technique is uncommon enough that it should be used only when your entire team is familiar with it.
Convert a nested if
to a set of if-then-else
s
If you think about a nested if
test critically, you might discover that you can reorganize it so that it uses if-then-else
s rather than nested ifs. Suppose you have a busy decision tree like this:
if ( 10 < quantity ) {
if ( 100 < quantity ) {
if ( 1000 < quantity ) {
discount = 0.10;
}
else {
discount = 0.05;
}
}
else {
discount = 0.025;
}
}
else {
discount = 0.0;
}
You can reorganize the code for better readability and reduced complexity:
if ( 1000 < quantity ) {
discount = 0.10;
}
else if ( 100 < quantity ) {
discount = 0.05;
}
else if ( 10 < quantity ) {
discount = 0.025;
}
else {
discount = 0;
}
Convert a nested if
to a case
statement
You can recode some kinds of tests, particularly those with integers, to use a case
statement rather than chains of if
s and else
s. You can’t use this technique in some languages, but it’s a powerful technique for those in which you can.
Factor deeply nested code into its own routine
If deep nesting occurs inside a loop, you can often improve the situation by putting the inside of the loop into its own routine. This is especially effective if the nesting is a result of both conditionals and iterations. Leave the if-then-else
branches in the main loop to show the decision branching, and then move the statements within the branches to their own routines.
The new code has several advantages. First, the structure is simpler and easier to understand. Second, you can read, modify, and debug the shorter while
loop on one screen; it doesn’t need to be broken across the screen or printed- page boundaries.
⚠️ Complicated code is a sign that you don’t understand your program well enough to make it simple.
A Programming Foundation: Structured Programming
The core of structured programming is a simple idea that a program should use only one-in, one-out control constructs (also called single-entry, single-exit control constructs).
A structured program progresses in an orderly, disciplined way, rather than jumping around unpredictably. You can read it from top to bottom, and it executes in much the same way.
The Three Components of Structured Programming
- Sequence
- A sequence is a set of statements executed in order. Typical sequential statements include assignments and calls to routines.
- Selection
- A selection is a control structure that causes statements to be executed selectively. The
if-then-else
statement is a common example.
- Iteration
- An iteration is a control structure that causes a group of statements to be executed multiple times. An iteration is commonly referred to as a “loop”.
- The core thesis of structured programming is that any control flow whatsoever can be created from these three constructs of sequence, selection, and iteration (Böhm Jacopini 1966).
How important is complexity?
The competent programmer is fully aware of the strictly limited size of his own skull; therefore he approaches the programming task in full humility
~ Edsger Dijkstra
Control-flow complexity is important because it has been correlated with low reliability and frequent errors.