- Contract
- Decide what information the method needs from the outside world,
and what information it returns to the outside world. Ask the following
questions:
- What kind of object, if any, does it operate on?
- In addition to the object it operates on, what other kind(s) of
information, if any, does it need in order to do its job?
- What kind of information, if any, does it return as a
"value"?
In Scheme, one writes down the answers in a comment, e.g.
;; contains? : list-of-symbol, symbol => boolean
In Java, you can write a similar comment:
// SymbolList.contains : Symbol => boolean
but, considering that essentially the same information will appear in
the method header anyway,
it may make more sense to simply ask yourself all these questions,
and then write the contract directly in the form of a method header.
- Header
- Translate your contract into Java syntax.
- If the method needs to be called from outside its class, make it
public
; otherwise, make it private
. (This is
an oversimplification, of course....)
- If the method doesn't need to operate on an existing object of its
class, make it
static
.
- The "kind of information returned" in the contract becomes the return
type (or "void" if none).
- The method name becomes the method name (surprise!).
- For each piece of "information needed", choose an appropriate
parameter name and put the type and parameter name inside the
parentheses.
-
I always write both the
starting and the ending braces of the method definition at this point,
because I know that if I don't, I'll forget and leave one of them out.
For example, the above "contains" example could be written
down as
class SymbolList {
...
boolean contains (Symbol toFind) {
...
}
...
}
- Examples
- Write down one or more examples of how you would use this method,
along with the "right answer" you expect it to produce.
This step is useful in at least three ways.
- It allows you to try
out the syntax people will use to invoke the method, and perhaps learn
that your first guess at the syntax is inconvenient to use, so you
should change the syntax before wasting a lot of time implementing it.
- It allows you to discover, early on, that your contract wasn't
quite right, and that you actually want different kinds of information
going in or out. If that happens, change the contract now and
re-write your examples accordingly; that will waste less time than going
on and having to change the contract later.
- It gives you a ready source of test cases, already in legal Java
syntax, thus removing one possible excuse for skipping the
Testing step, below.
Start with the simplest possible examples, then work up to more and more
complicated examples. Again, you can write your examples in a Java comment,
e.g.
// new Skewer().hasOnions() => false
// new Onion(new Skewer()).hasOnions() => true
// new Lamb(new Skewer()).hasOnions() => false
// new Lamb(new Onion(new Skewer())).hasOnions() => true
// new Onion(new Lamb(new Skewer())).hasOnions() => true
// new Lamb(new Tomato(new Skewer())).hasOnions() => false
but it may be more useful to put your examples in a static void
test() method, so you can run all your test cases easily.
- Template
- You actually have somewhat more information at this point than what will
fit into the header, and you can use this to fill in some of the body.
- For example, if your method is supposed to return
a String, you can be pretty sure that the last
statement in its body will be
return something-of-type-String;
You just need to figure out what the something-of-type-String
is, and you may be done with the body.
- If you're working with an abstract class that has several concrete
subclasses, it is highly likely (though not certain) that the abstract
class will have an abstract method declaration:
abstract class Shish {
...
public abstract boolean countOnions();
...
}
and each of the concrete subclasses will have a concrete implementation
of that method, e.g.
class Skewer extends Shish {
...
public boolean hasOnions() {
...
return something-of-type-boolean;
}
...
}
- If you're writing a method for a class that contains instance
variables, you're likely to need to use some of those instance variables.
If those instance variables are instances of classes, you are likely to
need to send them messages. In particular, if those instance variables
are instances of the same class that you're defining (or its
superclass), you are likely to need to send them the same
message you are currently defining, e.g.
class Tomato extends Shish {
Shish previous;
...
public boolean hasOnions () {
...
something about previous.hasOnions()
...
return something-of-type-boolean;
}
...
}
- Body
- Fill in the stuff in between the braces in the method definition.
This is the part that takes the most thinking, but
we've tried to avoid "blank-page syndrome"
by writing the contract, examples, header, and template first.
For example, in the above Tomato
example, once you've got the template in place, the only remaining
challenge is how to use the
answer to previous.hasOnions()
to get the answer to
the question you're trying to solve, so you can return
it as something-of-type-boolean.
- Testing
- Testing is essential if you want to have any faith that your
programs work. If you can't be bothered to test your programs, I can't
be bothered to grade them.
Since in Java, the fundamental compilation unit is the class, you won't
be able to test individual methods until you've written at least the
skeleton of a whole class.
However, you can add one method at a time to
that class skeleton, testing each one before adding the next.
Each time you've finished writing a method and want to test it,
- Compile your program. If it has syntax errors, read the error
messages carefully, figure out what they mean, fix them, and compile
again until there are no syntax errors.
- Run the method in question. If you're using a platform like
DrJava or BlueJ, you can invoke it directly; on some other platforms,
you'll need to write a static void main method whose sole
purpose is to run your test cases. In either case, if it produces
run-time errors, read the error messages carefully, figure out what
they mean, fix them, and compile and run again until there are no
syntax or run-time errors.
- For each of your test cases, observe the actual output. If it's
not what you expected, either you were expecting the wrong thing
(possible, though unlikely) or there's a bug in your program. Figure
out how the answer is wrong (not just that it's
wrong) and how this wrong answer could have happened, fix it, and
compile and run again until all test cases produce correct answers.
Remember, whenever you fix a bug, be sure to re-test the previously
working examples to make sure they still work!