Code Farms Inc

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:

  1. Avoid goto statements. This will eliminate the general network organization and unnecessary cycles.
  2. 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.

Code Farms Inc | www.codefarms.com | info@codefarms.com