Taming Dangerously Deep Nesting

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. 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, describes as Software's Primary Technical Imperative: Managing Complexity. That is reason enough to avoid deep nesting.

Taming Dangerously 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:

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 goto.

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:

Example 19-28. 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.

Example 19-29. 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:

Example 19-30. 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:

Example 19-31. 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:

Example 19-32. 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:

Example 19-33. 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:

Example 19-34. 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.

Example 19-35. 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. Guidelines for functional decomposition are given in "Divide and Conquer" in Design Practices.

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:

Example 19-36. 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.

Example 19-37. 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:

Example 19-38. 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.

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

Example 19-39. 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 goto)

  • Use guard clauses to exit a routine and make the nominal path through the code clearer (in Multiple Returns from a Routine)

  • Use exceptions (Exceptions)

  • Redesign deeply nested code entirely (this section)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.44.53