Lecture 13

Top-Down Program Design

We've seen how to write methods and programs to solve small problems.  In order to write programs to solve larger problems, we need a design stategy.  Top-down design is a strategy for solving larger problems.

The general approach of top-down design is to break up a large problem into smaller sub-problems.  Abstraction is a language feature that supports breaking up a large problem into small subproblems.

Data abstraction allows the definition of new data types.  In Java, classes support data abstraction.  Data types are useful for packaging up data that needs to be communicated between different parts of the program.

Procedural abstraction allows the definition of procedures (in Java, methods) to carry out computations required to solve subproblems.

The goal of a program design strategy is to identify the data types and procedures needed to solve an overall problem.  Top-down design helps mainly in identifying procedures, although it can help identify data types as well.

Example Problem

Here is an example problem that we can use in the discussion of top-down design.

  1. read in a sequence of temperatures

  2. compute the average temperature

  3. determine if the temperatures are strictly increasing

  4. determine the largest daily increase in temperature

Approach

  1. Sketch the overall computation at a high level in English, producing a pseudo-code description of the computation

  2. Identify central data structures: what data will be needed throughout the entire computation

  3. Identify procedures: sub-problems in the overall computation

  4. Determine inputs and output of each procedure

  5. Translate pseudo-code into code: subproblems become calls to methods which solve individual subproblems

  6. Add methods for subproblems

  7. Repeat steps 1-6 for each stub method

Application

Here is how we might apply top-down design to solving the example problem.

Sketch pseudo-code.  In this case, the problem description can serve as the pseudo-code.

prompt user for number of days
read temperature for each day
find the average temperature and print it
determine if temperatures are strictly increasing, if so print message
determine largest daily increase and print it

The phrases highlighted in green are sub-problems: these are good candidates to solve using methods.

Identify main data structures.  In this case, the main data used by the entire computation is the sequence of temperatures for each day.  We can represent an individual temperature using a double, and the overall sequence can be an array of doubles.  So, this is our central data structure: an array of double values.

Identify procedures.  We already identified the subproblems in the problem statement.  Each of these subproblems will become a method in the program.  The name of each method should directly reflect its purpose in the overall computation: for example:

promptUserForNumDays
readTemperatures
findAverageTemperature
determineIfStrictlyIncreasing
determineLargestDailyIncrease

Determine inputs and outputs of each procedure (method).  This is a crucial step.  Most methods will work by receiving data as input and producing data as output.  (Note that by "input" and "output" we are talking about data flowing into and out of methods, not program input and output such as System.in and System.out.)  The input data for a method will become its parameters, and the output data for a method will become its return value.

The central data structure will need to be passed to most methods as an input parameter.

Here is an analysis of the input parameters and output (return values) for each of the methods above:

Method Input (parameters) Output (return value)
promptUserForNumDays keyboard:Scanner numDays:int
readTemperatures keyboard:Scanner, numDays:int temps:double[]
findAverageTemperature temps:double[] avgTemp:double
determineIfStrictlyIncreasing temps:double[] isIncreasing:boolean
determineLargestDailyIncrease temps:double[] largestIncreaseDayIndex:int

Each item of data is given a name, indicating its purpose, and a type, indicating what kind of data it is.  A method can have any number of parameters, but only one return value.

The central data structure is the temps array: it is created by the readTemperatures method, and is provided as input to each subsequent method.

A Scanner object is passed as input to the promptUserForNumDays and readTemperatures methods, since both of these methods need to read input from the user.

Translate pseudo-code into Java code.  Based on your analysis of methods, data structures, and method inputs/outputs, you should be able to translate your pseudo-code into Java.  For example, here is the pseudo-code for the program's main method:

Scanner keyboard = new Scanner(System.in);
int numDays = promptUserForNumDays(keyboard);
double[] temps = readTemperatures(keyboard, numDays);
double average = findAverageTemperature(temps);
boolean isIncreasing = determineIfStrictlyIncreasing(temps);
if (isIncreasing) {
  System.out.println("Temperatures are increasing");
}
int largestIncreaseDayIndex = determineLargestDailyIncrease(temps);
printLargestIncrease(temps, largestIncreaseDayIndex);

Notice that a new method, printLargestIncrease, has been added.  This is the virtue of laziness in action: any time you would like to defer solving a problem until later, you should do so.  Just call a method, pass it the data it will require, and implement the method at a later time.

Add methods for subproblems.  For each subproblem you identified, add a method.  Use the method inputs and output to determine the parameters and return type.

Rather than immediately implementing each method, add the following statement to each one:

throw new UnsupportedOperationException("not implemented yet");

This will allow you to compile and run the program.  If an unimplemented method is called, the UnsupportedOperationException will occur and terminate the program.

Compile your program frequently.  Try not to allow syntax errors to accumulate.  Throwing an UnsupportedOperationException from unimplemented methods allows the program to compile and run even though you haven't implemented every method.

Here is the complete program so far:

import java.util.Scanner;

public class AnalyzeTemps {
  public static void main(String[] args) {
    Scanner keyboard = new Scanner(System.in);
    int numDays = promptUserForNumDays(keyboard);
    double[] temps = readTemperatures(keyboard, numDays);
    double average = findAverageTemperature(temps);
    boolean isIncreasing = determineIfStrictlyIncreasing(temps);
    if (isIncreasing) {
      System.out.println("Temperatures are increasing");
    }
    int largestIncreaseDayIndex = determineLargestDailyIncrease(temps);
    printLargestIncrease(temps, largestIncreaseDayIndex);
  }
  
  public static int promptUserForNumDays(Scanner keyboard) {
    throw new UnsupportedOperationException("not implemented yet");
  }
  
  public static double[] readTemperatures(Scanner keyboard, int numDays) {
    throw new UnsupportedOperationException("not implemented yet");
  }
  
  public static double findAverageTemperature(double[] temps) {
    throw new UnsupportedOperationException("not implemented yet");
  }
  
  public static boolean determineIfStrictlyIncreasing(double[] temps) {
    throw new UnsupportedOperationException("not implemented yet");
  }
  
  public static int determineLargestDailyIncrease(double[] temps) {
    throw new UnsupportedOperationException("not implemented yet");
  }
  
  public static void printLargestIncrease(double[] temps, int largestDailyIncreaseIndex) {
    throw new UnsupportedOperationException("not implemented yet");
  }
}

At this point we have a complete framework for the program, and to complete the program we just need to implement each method.