How to Design Class Hierarchies

Viera K. Proulx and Matthias Felleisen

January 6, 2003

0.1  Numbers and Expressions

Computers can deal with a variety of data, such as text, numerical values, images, sounds, mouse movement, etc. Numerical values are the most natural to understand both for people and for computers. We start by developing simple programs that deal with numeric computation.

Java provides a way to represent several different kinds of numerical values as PRIMITIVE TYPES. We will use two of them now: type int that represents integer values and double that represents real numbers (with fractional part). Numbers 5, -3, 122456, -3241 are int values, numbers 3.1415, -0.00234, 3456.2347, 12.5 are double values. Java provides the standard four arithmetic operators: +, -, *, and /. In addition, there is an operator % that is used to compute the remainder after the division by a given number. So, for example, the result of computing 20 % 3 is 2.

Parentheses can be used to group subexpressions to specify the order of the expression evaluation. The standard rules for expression evaluation typically work as expected, but it is better to write programs where the reader does not have to look up such details.

0.2  Variables and Functions

In algebra we learn to formulate dependencies between quantities using VARIABLE EXPRESSIONS. A variable is a placeholder that stands for an unknown quantity. For example, a disk of radius r has the the approximate area
3.14 * r * r
In this expression r stands for any positive number. If we now come across a disk with radius 5, we can determine its area by substituting 5 for r in the above formula and reducing the resulting expression to a number:
3.14 * 5 * 5 = 3.14 * 25 = 78.5 .
More generally, expressions that contain variables are rules that describe how to compute a number when we are given values for the variables.

A function in Java is such a rule. All descriptions of actions we want our program to perform are written as functions. The function definition describes the desired computation and the type of result it generates. The following function computes the area of a disk

    double areaOfDisk(double radius){
        return ( radius * radius * 3.1415 );
    }

The first line is the function header or function signature. The programmer selects a name that describes the intended purpose of this function. The double variable radius that appears in parentheses to the right of the function names is called function parameter. It describes the input to this function, the value that will be used in the subsequent computation. The keyword double signifies the fact that the value will be some real number - i.e. a number with a fractional part. Even if the radius is an integer value, it is treated as a number with fractional part, because the final result never is an integer. The first double in the function signature signifies the fact that the function will compute or (return) a value of the type double.

The signature is followed by the definition of the actual computation - enclosed in a pair of braces { }. The computation specifies that the function should return the result of performing the specified operation, after substituting the input value of the parameter radius for the value of the variable.

Once we have defined a function, we can use it. So, for example, we may write expressions whose operation is areaOfDisk replacing the variable radius by the desired input value and omitting the type specification for the input:

    areaOfDisk(5.0)

We also say that we APPLY areaOfDisk to 5.0.

The application of the function is evaluated by copying the expression that follows the return and by replacing the variable radius by the number we supplied (5.0):

    areaOfDisk(5.0)
    = ( 5.0 * 5.0 * 3.1415 )
    = ( 25.0 * 3.1415 )
    = 78.5375

The area of disk is that of the outer disk minus the area of the inner disk, which means that the function requires two unknown quantities: the outer and the inner radii. Let us call these unknown numbers outer and inner. Then the function that computes the area of ring is defined as follows:

    double areaOfRing(double outer, double inner){
        return areaOfDisk(outer) - areaOfDisk(inner);
    }

The three lines express that areaOfDisk is a function, that the function accepts two input parameters called outer and inner, that the result will be a double type value, and that the result is going to be the difference between areaOfDisk(outer) and areaOfDisk(inner). In other words, we have used both basic Java arithmetic operations and earlier defined Java functions in the definition of areaOfDisk.

When we wish to use areaOfDisk, we must supply two inputs:

    areaOfRing(5.0, 3.0)

The function is evaluated in the same manner as areaOfDisk(5.0). We copy the expression from the definition of the function and replace the variables with the numbers we supplied:

    areaOfRing(5.0, 3.0)

    = areaOfRing(5.0) - areaOfRing(3.0)

    = ( 5.0 * 5.0 * 3.1415 ) - ( 3.0 * 3.0 * 3.1415 )

The rest is plain arithmetic.

0.3  Word Problems

Programmers are rarely handed mathematical expressions to turn into functions. Instead they typically receive informal problem descriptions that often contain irrelevant and sometimes ambiguous information. The programmer's first task is to extract the relevant information and then to formulate appropriate expressions.

Here is a typical example:

Company XYZ & Co. pays its employees $12 per hour. A typical employee works between 20 and 65 hours per week. develop a function that determines the wage of an employee from the number of hours of work.

The last sentence is the first to mention the actual task: to write a function that determines one quantity based on some other quantity. More specifically, the program consumes one quantity, the number of hours of work, and produces another one, the wage in dollars. The first sentence implies how to compte the result, but does not state it explicitly. In this particular example, though, this poses no problem. if an employee works h hours, the wage is

12 * h 

Now that we have a rule, we can formulate a Java function:

    int wage(int h){
        return 12 * h;
    }

The function is called wage; its parameter h stands for the hours an employee works; the result is 12 * h, the corresponding wage.

0.4  Errors

When we write Java programs we must follow a number of carefully designed rules, which are a compromise between a computer's abilities and human behavior. For example, certain statements must end in a semicolon - that way the computer interpreting your program knows that you have finished that statement. The rules for writing functions are the following:

    return-type function-name ( par-type1 par-name1, ... ){
        return ...the computational expression... ;}

Syntax Errors: When the programmer does not follows the rules of the programming language syntax (its grammar), computer does not understand the program structure and signals a syntax error. Typical errors are the omission of a closing brace or a semicolon, misspelling of some of the words, or placing some parts of a statement out of order. To get familiar with how the compiler reacts to different types of errors, make intentional errors in your working program and observe the messages you get. Omit the closing brace, omit the semicolon, misspell a word, omit the word, use wrong type of argument in a function call, etc.

Runtime Errors: Once the program is successfully compiled it can be executed. That means that the computer now performs the actions the programmer specified. If the program expects a numeric value to perform a computation, but receives a word of text instead, it cannot continue and signals an error. A common error is an attempt to divide by zero.

Logical Errors: Computer performs the instructions given to it in a program exactly as stated. It does not know that the programmer made an arithmetic error such as

    double areaOfDisk(double radius){
        return ( radius + radius * 3.1415 );
    }

and computes the result as specified by the programmer. A programmer can catch such mistakes only by designing programs carefully and systematically.

0.5  Designing Functions

The preceding sections show that the development of a function requires many steps. We need to determine what's relevant in the problem statement and what we can ignore. We need to understand what the function consumes, what it produces, and how it relates inputs to outputs. We must know, or find out, whether Java provides certain basic operations for the data that our function is to process. If not, we might have to develop auxiliary functions that implement these operations. Finally, once we have a function, we must check whether it actually performs the intended computations. This might reveal syntax errors, run-time problems, or even logical errors.

To bring some order to this apparent chaos, it is best to set up and to follow a DESIGN RECIPE, that is, a step-by-step prescription of what we should do and the order in which we should do things. Based on what we have experienced thus far, the development of a program requires at least the following four activities: