19.4. Taming Dangerously Deep Nesting

 < Free Open Study > 

Excessive indentation, or "nesting," has been pilloried in computing literature for 25 years and is still one of the chief culprits in confusing code. Studies by Noam Chomsky and Gerald Weinberg suggest that few people can understand more than three levels of nested ifs (Yourdon 1986a), and 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, "Design in Construction," describes as Software's Primary 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 refactor code into simpler routines. The following subsections present several ways to reduce the nesting depth:


Simplify a nested if by retesting part of the condition If the nesting gets too deep, you can decrease the number of nesting levels by retesting some of the conditions. This code example has nesting that's deep enough to warrant restructuring:

C++ Example of Bad, Deeply Nested Code

if ( inputStatus == InputStatus_Success ) {    // lots of code    ...    if ( printerRoutine != NULL ) {      // lots of code      ...      if ( SetupPage() ) {         // lots of code         ...         if ( AllocMem( &printData ) ) {            // lots of code            ...         }      }    } }


Cross-Reference

Retesting part of the condition to reduce complexity is similar to retesting a status variable. That technique is demonstrated in "Error Processing and gotos" in Section 17.3.


This example is contrived to show nesting levels. The // lots of code parts are intended to suggest that the routine has enough code to stretch across several screens or across the page boundary of a printed code listing. Here's the code revised to use retesting rather than nesting:

C++ Example of Code Mercifully Unnested by Retesting
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. A reduction from four levels to two is a big improvement in readability, however, and is worth considering.

Simplify a nested if by using a break block An alternative to the approach just described is to define a section of code that will be executed as a block. If some condition in the middle of the block fails, execution skips to the end of the block.

C++ Example of Using a break 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 and when it has been adopted by the team as an accepted coding practice.

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-elses rather than nested ifs. Suppose you have a bushy decision tree like this:

Java Example of an Overgrown Decision Tree
if ( 10 < quantity ) {    if ( 100 < quantity ) {       if ( 1000 < quantity ) {          discount = 0.10;       }       else {          discount = 0.05;       }    }    else {       discount = 0.025;    } } else {    discount = 0.0; }

This test is poorly organized in several ways, one of which is that the tests are redundant. When you test whether quantity is greater than 1000, you don't also need to test whether it's greater than 100 and greater than 10. Consequently, you can reorganize the code:

Java Example of a Nested if Converted to a Set of if-then-elses
if ( 1000 < quantity ) {    discount = 0.10; } else if ( 100 < quantity ) {    discount = 0.05; } else if ( 10 < quantity ) {    discount = 0.025; } else {    discount = 0; }

This solution is easier than some because the numbers increase neatly. Here's how you could rework the nested if if the numbers weren't so tidy:

Java Example of a Nested if Converted to a Set of if-then-elses When the Numbers Are "Messy"
if ( 1000 < quantity ) {    discount = 0.10; } else if ( ( 100 < quantity ) && ( quantity <= 1000 ) ) {    discount = 0.05; } else if ( ( 10 < quantity ) && ( quantity <= 100 ) ) {    discount = 0.025; } else if ( quantity <= 10 ) {    discount = 0; }

The main difference between this code and the previous code is that the expressions in the else-if clauses don't rely on previous tests. This code doesn't need the else clauses to work, and the tests actually could be performed in any order. The code could consist of four ifs and no elses. The only reason the else version is preferable is that it avoids repeating tests unnecessarily.

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. Here's how to recode the example in Visual Basic:

Visual Basic Example of Converting a Nested if to a case Statement
Select Case quantity    Case 0 To 10       discount = 0.0    Case 11 To 100       discount = 0.025    Case 101 To 1000       discount = 0.05    Case Else       discount = 0.10 End Select

This example reads like a book. When you compare it to the two examples of multiple indentations a few pages earlier, it seems like a particularly clean solution.

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. This code needs to be improved by such a modification:

C++ Example of Nested Code That Needs to Be Broken into Routines
while ( !TransactionsComplete() ) {    // read transaction record    transaction = ReadTransaction();    // process transaction depending on type of transaction    if ( transaction.Type == TransactionType_Deposit ) {      // process a deposit       if ( transaction.AccountType == AccountType_Checking ) {          if ( transaction.AccountSubType == AccountSubType_Business )             MakeBusinessCheckDep( transaction.AccountNum, transaction.Amount );          else if ( transaction.AccountSubType == AccountSubType_Personal )             MakePersonalCheckDep( transaction.AccountNum, transaction.Amount );          else if ( transaction.AccountSubType == AccountSubType_School )             MakeSchoolCheckDep( transaction.AccountNum, transaction.Amount );       }       else if ( transaction.AccountType == AccountType_Savings )          MakeSavingsDep( transaction.AccountNum, transaction.Amount );       else if ( transaction.AccountType == AccountType_DebitCard )          MakeDebitCardDep( transaction.AccountNum, transaction.Amount );       else if ( transaction.AccountType == AccountType_MoneyMarket )          MakeMoneyMarketDep( transaction.AccountNum, transaction.Amount );       else if ( transaction.AccountType == AccountType_Cd )          MakeCDDep( transaction.AccountNum, transaction.Amount );    }    else if ( transaction.Type == TransactionType_Withdrawal ) {       // process a withdrawal       if ( transaction.AccountType == AccountType_Checking )          MakeCheckingWithdrawal( transaction.AccountNum, transaction.Amount );       else if ( transaction.AccountType == AccountType_Savings )          MakeSavingsWithdrawal( transaction.AccountNum, transaction.Amount );       else if ( transaction.AccountType == AccountType_DebitCard )          MakeDebitCardWithdrawal( transaction.AccountNum, transaction.Amount );    }    else if ( transaction.Type == TransactionType_Transfer ) {       <-- 1       MakeFundsTransfer(          transaction.SourceAccountType,          transaction.TargetAccountType,          transaction.AccountNum,          transaction.Amount       );    }    else {       // process unknown kind of transaction       LogTransactionError( "Unknown Transaction Type", transaction );    } } 

(1)Here's the TransactionType_Transfer transaction type.

Although it's complicated, this isn't the worst code you'll ever see. It's nested to only four levels, it's commented, it's logically indented, and the functional decomposition is adequate, especially for the TransactionType_Transfer transaction type. In spite of its adequacy, however, you can improve it by breaking the contents of the inner if tests into their own routines.

C++ Example of Good, Nested Code After Decomposition into Routines
while ( !TransactionsComplete() ) {    // read transaction record    transaction = ReadTransaction();    // process transaction depending on type of transaction    if ( transaction.Type == TransactionType_Deposit ) {       ProcessDeposit(          transaction.AccountType,          transaction.AccountSubType,          transaction.AccountNum,          transaction.Amount       );    }    else if ( transaction.Type == TransactionType_Withdrawal ) {       ProcessWithdrawal(          transaction.AccountType,          transaction.AccountNum,          transaction.Amount       );    }    else if ( transaction.Type == TransactionType_Transfer ) {       MakeFundsTransfer(          transaction.SourceAccountType,          transaction.TargetAccountType,          transaction.AccountNum,          transaction.Amount       );    }    else {       // process unknown transaction type       LogTransactionError("Unknown Transaction Type", transaction );    } }

Cross-Reference

This kind of functional decomposition is especially easy if you initially built the routine using the steps described in Chapter 9, "The Pseudocode Programming Process." Guidelines for functional decomposition are given in "Divide and Conquer" in Section 5.4.


The code in the new routines has simply been lifted out of the original routine and formed into new routines. (The new routines aren't shown here.) The new code has several advantages. First, two-level nesting makes the structure 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 screen or printed-page boundaries. Third, putting the functionality of ProcessDeposit() and ProcessWithdrawal() into routines accrues all the other general advantages of modularization. Fourth, it's now easy to see that the code could be broken into a case statement, which would make it even easier to read, as shown below:

C++ Example of Good, Nested Code After Decomposition and Use of a case Statement
while ( !TransactionsComplete() ) {    // read transaction record    transaction = ReadTransaction();    // process transaction depending on type of transaction    switch ( transaction.Type ) {       case ( TransactionType_Deposit ):          ProcessDeposit(             transaction.AccountType,             transaction.AccountSubType,             transaction.AccountNum,             transaction.Amount             );          break;       case ( TransactionType_Withdrawal ):          ProcessWithdrawal(             transaction.AccountType,             transaction.AccountNum,             transaction.Amount             );          break;       case ( TransactionType_Transfer ):          MakeFundsTransfer(             transaction.SourceAccountType,             transaction.TargetAccountType,             transaction.AccountNum,             transaction.Amount             );          break;        default:          // process unknown transaction type          LogTransactionError("Unknown Transaction Type", transaction );          break;    } }

Use a more object-oriented approach A straightforward way to simplify this particular code in an object-oriented environment is to create an abstract Transaction base class and subclasses for Deposit, Withdrawal, and Transfer.

C++ Example of Good Code That Uses Polymorphism
TransactionData transactionData; Transaction *transaction; while ( !TransactionsComplete() ) {    // read transaction record    transactionData = ReadTransaction();    // create transaction object, depending on type of transaction    switch ( transactionData.Type ) {       case ( TransactionType_Deposit ):          transaction = new Deposit( transactionData );          break;       case ( TransactionType_Withdrawal ):          transaction = new Withdrawal( transactionData );          break;       case ( TransactionType_Transfer ):          transaction = new Transfer( transactionData );          break;       default:          // process unknown transaction type          LogTransactionError("Unknown Transaction Type", transactionData );          return;    }    transaction->Complete();    delete transaction; }

In a system of any size, the switch statement would be converted to use a factory method that could be reused anywhere an object of Transaction type needed to be created. If this code were in such a system, this part of it would become even simpler:

C++ Example of Good Code That Uses Polymorphism and an Object Factory
TransactionData transactionData; Transaction *transaction; while ( !TransactionsComplete() ) {    // read transaction record and complete transaction    transactionData = ReadTransaction();    transaction = TransactionFactory.Create( transactionData );    transaction->Complete();    delete transaction; }

Cross-Reference

For more beneficial code improvements like this, see Chapter 24, "Refactoring."


For the record, the code in the TransactionFactory.Create() routine is a simple adaptation of the code from the prior example's switch statement:

C++ Example of Good Code for an Object Factory
Transaction *TransactionFactory::Create(    TransactionData transactionData    ) {    // create transaction object, depending on type of transaction    switch ( transactionData.Type ) {       case ( TransactionType_Deposit ):          return new Deposit( transactionData );          break;       case ( TransactionType_Withdrawal ):          return new Withdrawal( transactionData );          break;       case ( TransactionType_Transfer ):          return new Transfer( transactionData );          break;       default:          // process unknown transaction type          LogTransactionError( "Unknown Transaction Type", transactionData );          return NULL;    } }

Redesign deeply nested code Some experts argue that case statements virtually always indicate poorly factored code in object-oriented programming and are rarely, if ever, needed (Meyer 1997). This transformation from case statements that invoke routines to an object factory with polymorphic method calls is one such example.

More generally, complicated code is a sign that you don't understand your program well enough to make it simple. Deep nesting is a warning sign that indicates a need to break out a routine or redesign the part of the code that's complicated. It doesn't mean you have to modify the routine, but you should have a good reason for not doing so if you don't.

Summary of Techniques for Reducing Deep Nesting

The following is a list of the techniques you can use to reduce deep nesting, along with references to the sections in this book that discuss the techniques:

  • Retest part of the condition (this section)

  • Convert to if-then-elses (this section)

  • Convert to a case statement (this section)

  • Factor deeply nested code into its own routine (this section)

  • Use objects and polymorphic dispatch (this section)

  • Rewrite the code to use a status variable (in Section 17.3)

  • Use guard clauses to exit a routine and make the nominal path through the code clearer (in Section 17.1)

  • Use exceptions (Section 8.4)

  • Redesign deeply nested code entirely (this section)

 < Free Open Study > 


Code Complete
Code Complete: A Practical Handbook of Software Construction, Second Edition
ISBN: 0735619670
EAN: 2147483647
Year: 2003
Pages: 334

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net