Code Complete 2 – Steve McConnell – General Control Issues

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-elses

If you think about a nested if test critically, you might discover that you can reorganize it so that it uses if-then-elses 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 ifs and elses. 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.

Written by Nikola Brežnjak