Design Recipe for Java Classes

Each step of the design recipe will add some details to the previous one.

Define the problem

State, clearly and unambiguously in English, what the class is supposed to do or represent. If you can't do this in English, you have no hope of doing it in Java.

For example,
Class purpose:
Represent a student, with identifying information and grades.

Identify properties and behaviors
A property is a piece of information about class instances that satisfies one or both of the following requirements:
  1. It can be different from one class instance to another (e.g. name: each Student may have a different name)
  2. It can conceivably change over time (e.g. age or gradePointAverage).

A behavior is something a class instance can do, or something that can be done to a class instance. Typically, behaviors are carried out the same way for all instances of a given class, and this way doesn't change over time, but it may depend on the values of properties (above).

For example,
Class purpose:
Represent a student, with identifying information and grades.
Properties:
first and last names, student ID number, and GPA.
Behaviors:
A user of the class should be able to create a Student with specified name, ID, and GPA; to retrieve any of this information about an existing Student; and change the GPA.

In the case of a complicated class, there may be some parts that you don't want to tackle immediately; write down something about them anyway, and put them on a list of versions, e.g.
Student class:
Version 1:
Student class with basic instance variables, constructors, access methods, and toString()
Version 2:
add the ability to hold a bunch of grades in one Student; keep track of how many there are; add a new grade; change an existing grade; and compute the average of the existing grades
Version 3:
add a transcript that keeps track of what courses the student has already taken, and what grades were earned in each
Version 4:
add the ability to check prerequisites to see whether a student has already taken the prerequisites for a proposed course
etc.

Choose names and types

for the class, its properties, and its behaviors. Typically, each property becomes an instance variable, and each behavior becomes a method. This is a good time to write down other relevant information (e.g. in what units is a variable measured, what values are legal/illegal, etc.).

For example,
Class purpose:
Represent a student, with identifying information and grades.
Properties:
first and last names, student ID number, and GPA.
Behaviors:
A user of the class should be able to create a Student with specified name, ID, and GPA; to retrieve any of this information about an existing Student; and change the GPA. For debugging purposes, we should have a convenient way to display all the information about a given Student, and it should be possible to tell whether two Students are the same.
Class name:
Student
Instance variables:
String firstName
String lastName
int id
double gpa
Methods:
constructor Student(String firstName, String lastName, int id, double gpa)
access methods String getFirstName(),
String getLastName(),
int getID(),
double getGPA(),
void setGPA(double newGPA),
boolean same(Student other),
String toString()

Write test cases

Before you write any Java code for the class itself, figure out how you're going to test your class. The details of this depend on your approach to testing. Here are three approaches, each with its own advantages.

Test interactively, e.g. using the Interactions window of DrJava or ProfessorJ, or the CodePad or mouse in BlueJ

This approach is the easiest to do once, because it doesn't require writing any additional classes or methods. Unfortunately, if your class goes through a lot of modifications, you'll need to type in all the examples one by one, and check by hand that the answers are right, each time.

Student joe = new Student("Joe", "Schmoe", 1479025, 3.2);
joe.getFirstName() // should be "Joe"
joe.getLastName() // should be "Schmoe"
joe.getID() // should be 1479025
joe.getGPA() // should be approximately 3.2
joe.toString() // should be "Joe Schmoe (1479025), GPA 3.2"
joe.setGPA(3.3)
joe.getGPA() // should be approximately 3.3 now
joe.toString() // should be "Joe Schmoe (1470925), GPA 3.3"
Write boolean-valued testing methods which return whether or not the class passes all its tests

This approach allows you to test any desired behavior of the class interactively (as above) without typing in lots of individual test cases. If one of the methods returns false, however, you have to try each individual test case in that method to find out exactly what went wrong.

class TestStudent {
  private static boolean closeEnough (double num1, double num2) {
    return Math.abs(num1-num2) < 0.001;
    }
  public static boolean testGetters() {
    Student joe = new Student("Joe", "Schmoe", 1479025, 3.2);
    return joe.getFirstName().equals("Joe") &&
           joe.getLastName().equals("Schmoe") &&
	   joe.getID() == 1479025 &&
	   this.closeEnough(joe.getGPA(), 3.2);
    }
  public static boolean testSetter() {
    Student joe = new Student("Joe", "Schmoe", 1479025, 3.2);
    joe.setGPA(3.3);
    return joe.getFirstName().equals("Joe") &&
           joe.getLastName().equals("Schmoe") &&
	   joe.getID() == 1479025 &&
	   this.closeEnough(joe.getGPA(), 3.3);
    }
  public static boolean testToString() {
    Student joe = new Student("Joe", "Schmoe", 1479025, 3.2);
    return joe.toString().equals("Joe Schmoe (1479025), GPA 3.2");
    }
}
Write a JUnit test case

This is the industry-standard approach. Like the previous approach, it allows you to put tests for different behaviors in separate methods, but IDE's such as BlueJ and Eclipse "know" about it, and can easily run all your test methods, report which ones passed and which didn't, and for each one that failed, tell you about the first example in that method, what the answer should have been and what it was.

public class TestStudent extends junit.framework.TestCase
{
    private Student joe;

    protected void setUp()
    {
        this.joe = new Student("Joe", "Schmoe", 1479025, 3.2);
    }

    public void testGetters() {
        this.assertEquals(this.joe.getFirstName(), "Joe");
        this.assertEquals(this.joe.getLastName(), "Schmoe");
        this.assertEquals(this.joe.getID(), 1479025);
        this.assertEquals(this.joe.getGPA(), 3.2, 0.001);
    }
    
    public void testSetter() {
        this.joe.setGPA(3.3);
        this.assertEquals(this.joe.getFirstName(), "Joe");
        this.assertEquals(this.joe.getLastName(), "Schmoe");
        this.assertEquals(this.joe.getID(), 1479025);
        this.assertEquals(this.joe.getGPA(), 3.3, 0.001);
    }
    
    public void testToString() {
        this.assertEquals(this.joe.toString(), "Joe Schmoe (1479025),
GPA 3.2");
    }
} 
Write Java code

You'll probably want to do this in several small steps. For example, you might

  1. start with an empty class definition, with its javadoc comment:

     /**
       * Represents a student, with identifying information and grades.
       *
       * @author Stephen Bloch
       * @version 1, Feb. 23, 2004
       */
        class Student {
        }
     

  2. add instance variables:

     /**
       * Represents a student, with identifying information and grades.
       *
       * @author Stephen Bloch
       * @version 2, Feb. 23, 2004
       */
        class Student {
            /** Student's first and last names.
    	  * @since version 2, Feb. 23, 2004
    	  */
    	private String firstName, lastName;
            /** Student's student ID number.
    	  * @since version 2, Feb. 23, 2004
    	  */
    	private int id;
            /** Student's current GPA.
    	  * @since version 2, Feb. 23, 2004
    	  */
    	private double gpa;
        }
     

  3. For each method, go through the design recipe for methods. Be sure to do this in order: first the methods that don't depend on any others, then methods that depend on those, and so on, so you can test each individual method before going on to the next. Also, if some methods aren't needed at all until, say, version 3 of the class, you can postpone writing them (and comment out their test cases) until after you've tested and debugged versions 1 and 2 of the whole class.

Test the class
In the course of the design recipe for methods, you've already tested each of the methods individually. But you're not finished: it is possible that some non-obvious interaction among the methods causes incorrect behavior. So now you need to test the class as a whole.

You should already have lines in your testing method(s) that create a number of objects of the class, with different properties, try various individual methods on them, and check the results. Now start trying different sequences or combinations of operations on each object. If each such sequence of operations behaves as expected, you have reason to believe the class is correct. If not, you have a defect somewhere: you already have confidence in each method on its own, so the defect must be an interaction or misunderstanding between two or more methods (for example, there's a housekeeping chore to be done, and each method thinks the other is doing it so it doesn't get done, or both methods do it so it gets done twice).


Last modified: Thu Jun 1 15:41:42 EDT 2006
Stephen Bloch / sbloch@adelphi.edu