Section 6.11. Object-Oriented Design: Structured Programming


[Page 282 (continued)]

6.11. Object-Oriented Design: Structured Programming

Structured programming is the practice of writing programs that are built up from a small set of predefined control structures. As an overall approach to programming, structured programming has largely been superseded by the object-oriented approach. Nevertheless, its design principles are still relevant to the design of the algorithms and methods that make up a program's objects.

The principles of structured programming seem so obvious today that it may be difficult to appreciate their importance. In the 1960s and 1970s, one of the main controls used in programs was the infamous go-to statement, which could be used to transfer control of a program to any arbitrary location within it, and from there to any other arbitrary location, and so on. This led to incredibly complex and ill-formed programsso-called "spaghetti code"that were almost impossible to understand and modify.

Spaghetti code


Structured programming evolved in reaction to the unstructured software-development practices of the 1960s, which were fraught with budget overruns, costly delays, and failed products. One of the classic research results of that era was a 1966 paper by Boehm and Jacopini that showed that any program using go-to's could be represented by an equivalent program that used a sequence of two types of controls: if-else and while structures. Another influential paper, by Edgar Dikjstra ("GoTo Statement Considered Harmful"), pointed out the various ways in which the go-to statement could lead to impossibly complex programs.

The Pascal language, introduced by Nicklaus Wirth in 1971, was designed to promote structured programming techniques and became the language of choice in academic institutions because of its suitability as a teaching language. In Pascal, the go-to was replaced with the four structures that control the flow of execution in a program (Fig. 6.14):

  • Sequence: The statements in a program are executed in sequential order unless their flow is interrupted by one of the following control structures.


  • [Page 283]
  • Selection: The if, if-else, and switch statements are branching statements that allow choice through the forking of the control path into two or more alternatives.

  • Repetition: The for, while, and do-while statements are looping statements that allow the program to repeat a sequence of statements.

  • Method Call: Invoking a method transfers control temporarily to a named method. Control returns to the point of invocation when the method is completed.

Figure 6.14. Flowcharts of the four types of control structures. Each small rectangle represents a single executable statement.


No matter how large or small a program you write, its flow of control can be constructed as a combination of these four basic types of structures.

Preconditions and Postconditions

The Java language supplies us with a good collection of control structures, and Java's syntax constrains the way we can use them. One of the features of the four control structures is that each has a single entry point and exit (Fig. 6.14). This is an extremely important property. To grasp its importance, consider the following debugging problem:

k = 0;                            // 1. Unstructured code System.out.println("k= " + k);    // 2. k should equal 0 here goto label1;                      // 3. label2: System.out.println("k= " + k);    // 4. k should equal 1 here 


In this example a go-to statement is used to jump to label1, a label that marks a section of code somewhere else in the program. Suppose we're trying to determine how k has acquired an erroneous value and that its value is correct in line 2 of this sequence. Given the go-to statement on line 3, there's no guarantee that control will ever return to the println() statement on line 4. Thus, in unstructured code it is very difficult to narrow the scope of an error to a fixed segment of code. Because the go-to statement can transfer control anywhere in the program, with no guarantee of return, any segment of code can have multiple entry points and multiple exits.


[Page 284]

Now contrast the unstructured code with the following well-structured code:

k = 0;                            // 1. Structured code System.out.println("k= " + k);    // 2. k should equal 0 here someMethod();                     // 3. System.out.println("k= " + k);    // 4. k should equal 1 here 


In this case, we can be certain that control will eventually return to line 4. If k's value is erroneous on line 4, we can trace through someMethod() to find the error. Because any segment of a structured program has a single entry and exit, we can use a pair of println() statements in this way to converge on the location of the program bug.

Debugging with println()


An important implication of the single-entry/single-exit property is that we can use preconditions and postconditions to help us design and debug our code. The preceding example provided a simple example: The precondition is that k should equal 0 on line 2, and the postcondition is that k should equal 1 on line 4. Figure 6.15 shows some additional examples.

Figure 6.15. Using pre- and postconditions to document code.

int k = 0;                        // Precondition: k == 0 k = 5;                            // Assignment to k                                   // Postcondition: k == 5 int k = 0;                        // Precondition: k == 0 while (k < 100) {                 // While loop     k = 2 * k + 2; }                                   // Postcondition: k >= 100 /*  * factorial(n):  *   factorial(n) is 1 if n is 0  *   factorial(n) is n * n-1  n-2 * ... * 1 if n > 0  * Precondition:  n >= 0  * Postcondition:  *  factorial(n) = 1 if n = 0  *               = n * n-1  n-2  ... * 1 if n > 0  */ public int factorial(int n) {     if (n == 0)         return 1;     else {         int f = 1;                    // Init a temporary variable         for (int k = n; k >= 1; k--)  // For n down to 1             f = f * k;                // Accumulate the product         return f;                     // Return the factorial     } // else } // factorial() 


[Page 285]

In the first example, we use pre- and postconditions to define the semantics of an assignment statement. No matter what value k has before the assignment, the execution of the assignment (k = 5) will make the postcondition (k == 5) true.

In the second example, the postcondition follows from the semantics of the while loop. The loop-entry condition is k < 100, so when the loop exits the postcondition, (k >= 100) must be true.

The third example shows how pre- and postconditions can be used to design and document methods. The factorial(n) is defined for n 0 as follows:

factorial(n) is 1, if n == 0 factorial(n) is n * n-1 * n-2 * ... * 1, if n > 0 


In other words, the factorial of N is defined as the cumulative product of multiplying 1 times 2, times 3, and so on up to N. For example, if N is 5, then factorial(5) is 1 * 2 * 3 * 4 * 5 = 120.

Note how the factorial computation is done in the method. The variable f, which is used to accumulate the product, is initialized to 1. Then on each iteration of the for loop, f is multiplied by k and the product is assigned back to f. This is similar to the way we accumulate a sum, except that in this case we are accumulating a product.

The precondition on the factorial() method represents the condition that must be true in order for the method to work correctly. Factorial is undefined for n < 0, so it is important that n be greater than or equal to 0 whenever this method is called. Given that the precondition holds, the postcondition gives a precise specification of what must be true when the method is finished.

Design: Defensive Programming

The pre- and postconditions for a method can be used to design defensive codethat is, code that guards against errors. For example, what action should factorial() take if its precondition fails to hold? In Java, the best way to handle this situation is to throw an IllegalArgumentException, as the following example illustrates:

public int factorial(int n) {   if (n < 0)                       // Precondition failure    throw new IllegalArgumentException("Factorial:"+ n);   if (n == 0)     return 1;   else {     int f = 1;                     // Init a temporary variable     for (int k = n; k >= 1; k--)   // For n down to 1       f = f * k;                   // Accumulate the product     return f;                      // Return the factorial   } } // factorial() 



[Page 286]

An exception is an erroneous condition (an error) that arises during the running of a program. An Exception is an object that encapsulates information about the erroneous condition. A program can throw an Exception, thereby stopping the program, when an erroneous condition is detected. In this example, we create a new IllegalArgumentException that would report the illegal value of n with something like the following error message:

Exception in thread "main" java.lang.IllegalArgumentException:           Factorial: -1    at Test.factorial(Param.java:5)    at Test.main(Param.java:18) 


You have undoubtedly already encountered thrown exceptions during program development. Java has an extensive hierarchy of Exceptions, which we will cover in some depth in Chapter 11. For now, however, we just note how to use the IllegalArgumentException. As its name implies, an IllegalArgumentException is used when an argument in a method call is not legal.

Rather than continuing the program with an erroneous data value, throwing an exception causes the program to stop and print an error message. Determining whether an argument is legal or illegal is an important use of the method's preconditions. The failure of the precondition in factorial() points to a problem elsewhere in the program, because it is doubtful that the program deliberately passed a negative value to factorial(). The discovery of this error should lead to modifications in the part of the program where factorial() was invokedperhaps to some validation of the user's input:

int num = Integer.parseInt(textIn.getText()); if (num >= 0)                     // If factorial() precondition valid     factNum = factorial(num);     // Compute factorial else     System.out.println("Error");  // Report input error 


That would be the traditional way to handle this kind of error.

Using Pre- and Postconditions

The use of preconditions and postconditions in the ways we have described can help improve a program's design at several distinct stages of its development:

  • Design stage: Using pre- and postconditions helps to clarify the design and provides a precise measure of correctness.

  • Implementation and testing stage: Test data can be designed to demonstrate that the preconditions and postconditions hold for any method or code segment.

  • Documentation stage: Using pre- and postconditions to document the program makes the program more readable and easier to modify and maintain.


  • [Page 287]
  • Debugging stage: Using pre- and postconditions provides precise criteria that can be used to isolate and locate bugs. A method is incorrect if its precondition is true and its postcondition is false. A method is improperly invoked if its precondition is false.

Like other programming skills and techniques, learning how to use pre- and postconditions effectively requires practice. One way to develop these skills is to incorporate pre- and postconditions into the documentation of the methods you write for laboratories and programming exercises. Appendix A provides guidelines on how to incorporate pre- and postconditions into your program's documentation. However, it would be a mistake to get in the habit of leaving the identification of pre- and postconditions to the documentation stage. The method's documentation, including its pre- and postconditions, should be developed during the design stage and should play a role in all aspects of program development.

Effective Program Design

What we are really saying here is that using pre- and postconditions forces you to analyze your program's logic. It is not enough to know that a single isolated statement within a program works correctly at the present time. You have to ask yourself whether it will continue to work if you change some other part of the program, and whether other parts of the program will continue to work if you revise it. No matter how clever you are, you cannot possibly keep an entire model of a good-sized program in your head at one time. It is always necessary to focus on a few essential details and leave aside certain others. Ideally, what you hope is that the details you've left aside for the moment are not the cause of the bug you're trying to fix. Using pre- and postconditions can help you determine the correctness of the details you choose to set aside.

Effective Design: Pre- and Postconditions

Pre- and postconditions are an effective way of analyzing the logic of your program's loops and methods. They should be identified at the earliest stages of design and development. They should play a role in the testing and debugging of the program. Finally, they should be included, in a systematic way, in the program's documentation.


Java Programming Tip

Develop your program's documentation at the same time that you develop its code, and include the pre- and postconditions in the documentation.


As the programs you write become longer and more complex, the chances that they contain serious errors increase dramatically. There's no real way to avoid this complexity. The only hope is to try to manage it. In addition to analyzing your program's structure, another important aspect of program design is the attempt to reduce its complexity.

Effective Design: Reducing Complexity

Design your programs with an aim toward reducing their complexity.



[Page 288]

Perhaps the best way to reduce complexity is to build your programs using a small collection of standard structures and techniques. The basic control structures (Fig. 6.14) help reduce the complexity of a program by constraining the kinds of branching and looping structures that can be built. The control structures help to manage the complexity of your program's algorithms. In the same way, the following practices can help reduce and manage the complexity in a program.

Java Programming Tip: Standard Techniques

Acquire and use standard programming techniques for standard programming problems. For example, using a temporary variable to swap the values of two variables is a standard technique.


Java Programming Tip: Encapsulation

Use methods wherever appropriate in your own code to encapsulate important sections of code and thereby reduce complexity.


Java Programming Tip: Code Reuse

Instead of reinventing the wheel, use library classes and methods whenever possible. These have been carefully designed by experienced programmers. Library code has been subjected to extensive testing.


Self-Study Exercises

Exercise 6.18

Identify the pre- and postconditions on j and k where indicated in the following code segment:

int j = 0; k = 5; do {     if (k % 5 == 0) {                      // Precondition         j += k;         k--;     }     else k *= k; } while (j <= k);                     // Postcondition 


Exercise 6.19

Identify the pre- and postconditions for the following method, which computes xn for n . 0:

public double power(double x, int n) {     double pow = 1;     for (int k = 1; k <= n; k++)         pow = pow * x;     return pow; } // power() 



[Page 289]

Special Topic: What Can Be Computed?

Did you ever wonder whether there are problems that cannot be solved by a computer, no matter what kind of control structures are used? Well, back in 1939, in his seminal paper titled "On Computable Numbers," Alan Turing proved that indeed there are an infinite number of unsolvable problems. Prior to this, mathematicians and logicians thought that all problems could be solved. So Turing's proof was quite a blow!

To help prove this point, Turing defined an abstract computer that has come to be known as a Turing machine. A Turing machine has an alphabet of symbols; a read/write head; an infinitely long tape on which the read/write head can write symbols, and from which it can read symbols; and a control unit, that controls the movement and action of the read/write head. Note that the Turing machine elements correspond to key components of a computer, although Turing invented this concept a decade before the first computers were developed. The read/write head corresponds to a computer's central processing unit (CPU). The tape corresponds to the computer's memory. And the control unit corresponds to the computer program.

A Turing machine represents a purely abstract concept of computation. It represents the pure idea of an algorithmic solution to a problem. Equipped with this concept, Turing was able to prove that there are unsolvable problemsthat is, problems for which no algorithm can arrive at a solution.

One such problem is the halting problem. This problem asks whether an algorithm can be devised to determine whether an arbitrary program will eventually halt. If there were such an algorithm, it could be used to detect programs that contain infinite loops, a service that might be really helpful in an introductory computing lab, among other places! But, alas, there can be no such algorithm.

Here's an outline of a proof that shows that the halting problem is unsolvable. (This particular version of the proof was suggested by J. Glenn Brookshear in Computer Science: An Overview, Benjamin-Cummings, 1985.)

Suppose you had a program, P, that solves the halting problem. That is, whenever P is given a self-halting program, it sets a variable isTerminating to true, and otherwise it sets isTerminating to false. Now let's create a new version of P, named P', which is identical to P except that right after where P sets isTerminating to true or false, P' contains the following loop:

while (isTerminating == true); // Infinite if isTerminating true 


In other words, if the input to P' is a self-terminating program, then P' will enter an infinite loop and will not terminate. Otherwise, if a non-self-terminating program is input to P', P' will skip the loop and will terminate.

Now what if we give a representation of P' to itself? Will it halt? The answer generates a contradiction: If P' is a self-terminating program, then when it is input to itself, it will not terminate. And if P' is not self-terminating, then when it is input to itself, it will terminate. Because our assumption that P solves the halting problem has led to a contradiction, we have to conclude that it wasn't a very good assumption in the first place. Therefore, there is no program that can solve the halting problem.

The topic of computability is a fundamental part of the computer science curriculum, usually taught in a sophomore- or junior-level theory of computation course.





Java, Java, Java(c) Object-Orienting Problem Solving
Java, Java, Java, Object-Oriented Problem Solving (3rd Edition)
ISBN: 0131474340
EAN: 2147483647
Year: 2005
Pages: 275

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