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.
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:
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:
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:
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:
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"
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:
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 ); } }
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:
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.
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)
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)
18.224.44.53