1.3 THE LEGACY OF STRUCTURED
PROGRAMMING
Using a simple example involving two
mortgages, this chapter compares three different coding styles: unstructured
C, structured C, and object-oriented implementation. It also shows that
the main contribution of structured programming was the elimination
of cycles from code logic.
Back in the 1960s, computer programs
were difEcult to read. The primitive languages (FORTRAN and often even
assembly languages) frequently used if and goto statements, resulting
in "spaghetti-like" code. Programs were essentially networks of statements,
where the execution could jump freely from one statement to another,
using conditional or unconditional jump statements.
This situation led the entire industry
to use flow charts, [YOU]. The flow chart was a diagram which represented
the program as a directed graph that connected sequential sections of
the code (rectangular boxes). (I am purposely using the past tense,
since very few people use flow charts today.) The execution could branch
at the if statements (diamonds), or could jump to any other section
of the code, using the goto statement (simple arrows).
Flow charts helped programmers understand
the logic of their code. Hand drawn flow charts, and later automatically
generated diagrams, became a standard part of every program documentation.
As usual, when programs become difficult to handle, programmers reach
for additional tools to decrease the complexity.
A major breakthrough came with the introduction
of structured programming. The two key ideas in structured programming
are:
- Avoid goto statements. This will eliminate
the general network organization and unnecessary cycles.
- Structure the program in smaller subroutines
or functions which can be easily understood and tested. This is equivalent
to hierarchical decomposition of large problems, which were often
handled in a flat manner in the past.
Let us examine these concepts using a small,
simple example.
The two-mortgage problem
Assume that you borrow money for your new
house. The total amount you need comes in two mortgages, with two different
interest rates. The bank insists that you pay off the first mortgage,
and only then start paying off the second mortgage, see Figure 1.1. You
want to know, for a given fixed monthly payment, how many payments will
be required to pay off both mortgages, and what the last payment will
be.
Instead of using a general formula,
we will simulate the account and count the monthly payments. For example,
if you start with a $40,000 first mortgage at 5.5%, $20,000 second mortgage
at 8.0%, and opt for monthly payments of $750, the program will run
through the following calculation:
Using the interest formula (1 + m)**12
= (1 + y), where m is the monthly compounded interest, and y is the
yearly interest, we have m = (1 + y)**(1./12) - 1. The monthly interest
for the first mortgage is 0.4472, and the monthly interest for the second
mortgage is 0.6434%.
Before the first payment, the principals
grow to 40,000* 1.004472 and 20,000*1.006434. The payment of $750 is
subtracted from the first mortgage 40,178.87 - 750 = 39,428.87.
Before the second payment, the principals
grow to 39,428.87*1.004472 and 20,128.68*1.006434. The payment of $750
is subtracted from the first mortgage 39,605.18 - 750= 38,855.18.
Figure 1.1 Problem of two mortgages
(values not to scale).
The principal of the second mortgage
increases while the first mortgage is being paid off. Before payment
62, the principals (including the last month's interest) are:
First mortgage = $48.38
Second mortgage = $29,765.93
Payment 62 closes the first mortgage and
subtracts (750 - 48.38) from the second mortgage. Subsequent payments
are all applied to the second mortgage, until the last payment of $538.91
discharges the mortgage. There is total of 107 payments including the
last one: it takes 9 years less one month to pay off the loan.
If we code the problem in the old, unstructured
style, just following the problem description, we may end up with a
program like this:
Example 1.1
#include <stdio.h>
#include <math.h>
int main(void){
double p1,p2,i1,i2,m,x;
int n;
printf("principal of the 1-st mortgage="); scanf("%lf",&p1);
printf("interest on the 1-st mortgage="); scanf("%lf",&i1);
printf("principal of the 2-nd mortgage="); scanf("%lf",&p2);
printf("interest on the 2-nd mortgage="); scanf("%lf",&i2);
printf("monthly payment ="); scanf("%lf",&m);
/* convert yearly percents into monthly interest */
i1=pow(1+0.01si1,1./12) -1:
i2=pow(1+0.01ti2,1./12) -1;
n=0;
REPEAT:
if(p1+p2<=0)goto FINISHED;
if(il*pl+i2*p2>=m)goto TOO_LOW;
n++;
p1=p1+i1*p1;
p2=p2+i2*p2;
x=m;
if(p1<=0)goto SECOND;
if(p1<=x){x=x-p1; p1=0;}
else {p1=p1-x; x=0;}
SECOND:
if(x<=0)goto REPEAT;
if(p2<=0)goto FINISHED;
if(p2<=x){x=x-p2; p2=0;}
else {p2=p2-x; x=0;}
goto REPEAT;
FINISHED:
printf("\n%d payments of $ %8.2f\n".n-1,m);
printf("last payment $ %8.2f\n",m-x);
goto EXIT;
TOO_LOW:
printf("\nmonthly payment too low to pay the interest\n");
EXIT:
return(0);
}
Figure 1.2 Flow chart for the two-mortgage
problem, emphasizing the network character of the logic.
The reasoning behind this code style
is that the program can jump from any statement to any other statement
using conditional or unconditional goto statements. The program is a
network, without any sense of flow or direction.
Figure 1.2 demonstrates this graphically.
The standard flow chart is quite confusing because of the lack of any
pattern or flow.
If we restructure the same code so that
we avoid goto statements, there is a remarkable improvement in the clarity
of the code:
Example 1.2
#include <stdio.h>
#include <math.h>
int main(void){
double p1,p2,i1,i2,m,x;
int n;
printf("principal of the 1-st mortgage="); scanf("/Olf",&p1);
printf("interest on the 1-st mortgage="); scanf("%lf",&i1);
printf("principal of the 2-nd mortgage="); scanf("%lf",&p2);
printf("interest on the 2-nd mortgage="); scanf("%lf",&i2);
printf("monthly payment ="); scanf("%lf",&m);
/* convert yearly percents into monthly interest */
i1=pow(1+0.01*i1,1./12) -1;
i2=pow(1+0.01*i2,1./12) -1;
n=0;
while(p1+p2>0){
if(i1*p1+i2*p2>=m)break;
p1=p1+i1*p1;
p2=p2+i2*p2;
n++;
x=m;
if(p1>0){
if(p1<=x){x=x-p1; p1=0;}
else {p1=p1-x; x=0;}
}
if(x>0){
if(p2>0){
if(p2<=x){x=x-p2; p2=0;}
else {p2=p2-x; x=0;}
}
}
}
if(p1+p2<=0){
printf("\n%d payments of $ %8.2f\n",n-1,m);
printf("last payment $ %8.2f\n",m-x);
}
else printf("\nmonthly payment too low to pay the interest\n");
return(O);
}
Compare the random character of the diagram
in Figure 1.2 with the organized flow in Figure 1.3, which starts at the
upper left corner, and moves toward the lower right corner with the exception
of the while loop. The concept of a loop describes a certain behavioral
pattern, and is therefore more acceptable to the human mind than many
simple but random jumps. Loops also permit more rigorous analysis of program
logic, including the possibility to prove that a loop will terminate [GRI],
[DIN]. Limiting cycles to loops makes programs more comprehensible.
The representation in Figure 1.3 is
not of a general graph, where any statement can connect to any other
statement, but rather of a lattice organization, where a statement always
connects only to the statements below it, with the exception of loops.
This style of organization is much easier to comprehend, and consequently
easier to debug and maintain.
The program from Example 1.2 can be
further improved by breaking the one relatively large program into smaller
functions. This second step of making a program well structured will
be shown later (see Example 1.4).
A directed graph can be said to have
a flow, if there are no cycles. A graph without cycles is a lattice.
When making a network structured, we try to avoid cycles, and if cycles
must be present, we isolate them and make them clearly visible exceptions
(calling them feedback loops, while loops, or for loops).
When we implement the mortgage problem
in an object-oriented style, we get another order of improvement. Each
mortgage hides its own data: the operations on mortgages are localized
and can be independently tested. The main program is shorter, and makes
more sense, even on first reading:
Example 1.3
#include <iostream.h>
#include <math.h>
class mortgage {
double principal; /I current principal
double interest; /I monthly interest
public:
mortgage(char *mortName);
double princip(void){return(principal);} // current principal
double inter(void){return(principal*interest);} // current monthly interest
double payment(double m); // monthly transaction, returns amount paid
mortgage::mortgage(char *mortName){
cout << "principal of " << mortName << " ="; cin >> principal;
cout << "interest on " << mortName << " ="; cin >> interest;
interest=pow(1+O.O1*interest,1./12)-1.;
}
mortgage::payment(double m){ double r;
principal=principal+principal*interest;
if(m<principal)r=m; else r=principal;
principal=principal-r;
return(r);
}
int main(void) {
double m,x;
int n;
mortgage *m1,*m2;
m1=new mortgage("1-st mortgage");
m2=new mortgage("2-nd mortgage");
cout << "monthly payment ="; cin >> m;
for(n=0;m1->princip()+m2->princip()>0; n++){
if(m1->inter()+m2->inter()>=m){x=0: break;}
x=m1->payment(m);
x m2->payment(m-x);
}
if(x>0) {
cout << "\n" << n-l << " payments of $ " << m;
cout << "\nlast payment $ " << x;
}
else cout << "\nmonthly payment too low to pay the interest";
return(0);
}
Figure 1.3 In the structured form of
the program, there is a clear flow from the upper left corner to the lower
right corner, except for the feedback caused by the while
loop (bold line).
Success may lead to a false feeling
of security. It seems as if all the problems of the unstructured code
have been resolved by switching to an object-oriented style. The truth
however, is that these problems have been solved only at the code level,
while new problems of a similar nature may have been created at a much
higher level. The object-oriented paradigm, which is based on the idea
that any object may communicate with another object, is essentially
a network model. Instead of dealing with a network of code statements,
we may have a network of classes. This cannot be seen from the two-mortgage
example because it is too simple (it has only one class).
This problem is currently under intense
investigation. For example, Weber (keynote address in [ADV], p. 5) recommends
uniformity as one of two prime restrictions to be imposed on reusable
systems. Uniformity implies, besides other requirements, that "software
architecture must result in an acyclic directed graph as the underlying
composition scheme in which the nodes represent the reusable components
and the directed arcs point to the subordinate components." How to make
a network of classes more structured is one of the main themes of this
book; we will return to it in the following chapters.

|