ECE-1021

HOMEWORK #5B Solution Notes

(Last Mod: 27 November 2010 21:38:43 )

ECE-1021 Home


PROGRAM B - How much did that really cost? - modular

If the Top-Down design process was done well, then the pseudocode should suggest fairly obvious ways in which to modularize the code. Each identifiable task is a potential candidate to be turned into a function, although whether it is truly wise to do so is part of the art of programming. Similarly, each SET statement could potentially be turned into a function but, again, deciding whether or not to do so is up to the programmer. If the expression is complicated or if it involves more than extremely simply logic, then significant thought should be given to making it a function.

Keep in mind that there are lots of ways to modularize a program and lots of ways to accomplish a given task. The following represents one possible way and was crafted so as to provide examples of a number of different ways of doing things - such as passing a variable by reference or using the return value of a function to get information from the called function back to the calling function.

Top Level - Original:

  1. TASK: Get Input Data from User.

  2. TASK: Generate a Month-by-Month Table until item is paid off.

  3. TASK: Generate a Final Summary after the item is paid off.

Top Level - Modularized:

  1. CALL: GetInputData()  // Get Input Data from User.

  2. CALL: MonthByMonthTable() // Generate a Month-by-Month Table until item is paid off.

  3. CALL: FinalSummary() // Generate a Final Summary after the item is paid off.

This will be the backbone of our main() function.

We now need to identify what the arguments for each of these functions needs to be. Looking at the pseudocode, this becomes quite straightforward:

GetInputData() doesn't need any values passed to it but it must make values entered by the user available to main(). Therefore we need to pass these values by reference so that the function can change them.

MonthByMonthTable() needs the values set by the GetInputData() function. We also need to pass our running total variables by reference so that the program can change them. We will have the function return the number of months required to pay of the account.

FinalSummary() only needs the four values that it needs to print out (and, in point of fact, it actually only needs one of the running total variables as it can calculate the other one). It doesn't need to return any information to main().

With just this much development, we are actually in a position to write our main() function:

void GetInputData(double *price, double *apr, double *fraction, double *minimum);

int  MonthByMonthTable(double price, double apr, double fraction, double minimum,

                       double *TotalFinance, double *TotalCost);

void FinalSummary(double price, int months, double TotalFinance, double TotalCost);

 

int main(void)

{

    int months;

    double purchase;

    double apr, fraction, abs_minimum_pmt;

    double TotalFinance, TotalCost;

 

    GetInputData(&purchase, &apr, &fraction, &abs_minimum_pmt);

 

    months = MonthByMonthTable(purchase, apr, fraction, abs_minimum_pmt,

                               &TotalFinance, &TotalCost);

 

    FinalSummary(purchase, months, TotalFinance, TotalCost);

 

    return(0);

}

What's particularly important to note is that, at this point, we wave a complete main() function and the complete function prototypes for all of the functions invoked by main() and we have accomplished all of this with only the top level description of our Top Down design approach. We haven't begun to consider how we will get input from the user, how we will compute the data needed for the month-by-month table, how we will output that table, how we will compute the values needed for the final summary or how we will output that summary .

Before we proceed any further, we must decide which course our Top Down design process is going to take. The two extremes constitute would could be called a "depth-first" approach and a "breadth-first" approach.

In a depth-first approach, we take each task and get it fully implemented before moving on to the next task. Each time we decide to break a task into subtasks we tackle those subtasks one at a time completing each before proceeding to the next. If we choose this approach, we generally focus a lot of effort on testing each lower level function before integrating it into the final whole. For instance, in developing this program we might not have written the main() function at all - we might have stopped once we had identified the three tasks. We would have then picked one of them (not necessarily the first one) and gotten it working completely before spending much effort on the other two.

The breadth-first approach takes the opposite approach in that it focuses only on developing a given task far enough that it is broken into the next lower level of detail before moving on to the next task. For instance, in writing main() above we developed each of the three tasks only far enough to identify what information each needed and where that information would come from. In this approach, testing is down quite differently - we generally write "placekeeper" functions for the tasks that haven't been tackled yet. Those functions return simple values, frequently in very inflexible hard-coded implementations. But they allow us to see if the higher level code works properly if it is given proper values from the lower level functions. The goal is that by the time that the lowest level - and hence simplest - functions are finally implemented that the complete program functionality has been verified with a high degree of confidence.

So which approach is better? That is certainly an ongoing topic of discussion and debate In practice we usually use something in between and some tasks might be developed more along a depth-first approach and others might be more akin to a breadth-first approach. Commonly the upper most few levels are tackled breadth-first so that a very good feel for what the requirements of each of the high level tasks is. This usually helps the developer refine the requirements and get a good feel for some of the subtler issues and start thinking about special cases and the eventual testing that will be needed. Then, once that is in place, the approach switches over to a depth-first approach to capitalize on the efficiency that narrowly concentrated and focused effort can bring to bear on a problem once that problem is clearly defined.


Breadth-first development

For now, let's continue with a breadth first development. We'll write very simple placekeeper versions of all of the functions so that our main() works right away. We can then work on each function one-by-one to get it to do everything that it is supposed to do. Our initial functions might be nothing more than:

void GetInputData(double *price, double *apr, double *fraction, double *minimum)

{

    *price = 2500.0;

    *apr = 18.9/100.0;

    *fraction = 2.0/100.0;

    *minimum = 10.0;

   

    return;

}

 

int  MonthByMonthTable(double price, double apr, double fraction, double minimum,

                       double *TotalFinance, double *TotalCost)

{

    printf("MONTH BY MONTH SUMMARY:\n");

    *TotalFinance = 300.0;

    *TotalCost = price + *TotalFinance;

 

    return(37);

}

 

void FinalSummary(double price, int months, double TotalFinance, double TotalCost)

{

    printf("FINAL SUMMARY:\n");

    printf("Months: %i\n", months);

    printf("Price: %f\n", price);

    printf("Total Finance Charges: %f\n", TotalFinance);

    printf("Total Cost of Purchase: %f\n", TotalCost);

   

    return;

}

At this point, the first function supplies data without interacting with the user. This is very possibly very handy as it means we don't have to type in the data each time we run the program as we are developing the rest of the code. When we want to try different values for the parameters we simply change them within this function. At a suitable point, we can come back and add in the appropriate prompts and scanf() statements to actually get the data from the user. It's worthwhile to point out that even our placekeeper function has been written with the thought of what subsequent development on it will have to contend with - notice that we have used the values as we expect the user to enter them. As a result,we have anticipated the need for this function to divide the apr and fraction variables by one hundred. By hard coding this operation, we have a built in reminder of what this function will have to do at some point.

Similarly, the third function prints out the final data with no attempt to make it presentable. We don't need it to be pretty to determine if it is correct and at any point we can dress it up. The second function, which is where all the work is done, presently does nothing but set those variables needed by the third function to hard coded values. That's enough to see if the basic information passing from one function to the next is up and working.

So at what point should the first and third functions be fleshed out and finished? That's part of the art of engineering and is completely up to the developer. Some might wish to flesh out the straightforward functions immediately to wring every bit of understanding they can out of doing so. Others might choose the same approach so as to immediately have quite a bit of progress to show their supervisors. Others yet may still choose the same approach so that the rapid initial progress serves as a confidence builder. On the other hand, putting these functions off until the end has a similar host of potential benefits. Similarly many programmers make no particular plan regarding the implementation of these straightforward functions figuring that there will come a time when fleshing out certain ones will just naturally arise. They may also use them to fill in the gaps when they are struggling with one of the more difficult tasks in the belief (usually sound) that stepping back from a problem and shifting their attention to something else - even for a few minutes or hours - will give them the time they need to come at the problem fresh later.


As already noted, the first and third functions are already at a point that they can be implemented with little more development, so let's focus on the second function.

int  MonthByMonthTable(double price, double apr, 

                       double fraction, double minimum,

                       double *TotalFinance, double *TotalCost);

We've already dealt with the issue of what information gets passed and what information gets returned, but let's summarize it here for reference.

Inputs: 

Outputs:

Pseudocode for this TASK:

TASK: Generate a Month-by-Month Table until item is paid off.

  1. TASK: Output Table Header (first row of table)

  2. TASK: Initialize variables for first month

  3. TASK: Initialize variable for running totals

  4. WHILE (balance > 0)

    1. TASK: Update variables for that month's activity

    2. TASK: Update running totals

    3. TASK: Print out activity for that month

Reasonable candidate tasks that might get turned into functions are Tasks 1, 4.1, and 4.3.