Each step of the design recipe will add some details to the previous one.
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,
|
name
: each Student may have a different
name) 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,
|
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:
|
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,
|
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.
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" |
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"); } } |
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"); } } |
You'll probably want to do this in several small steps. For example, you might
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 { }
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; }
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.
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).