Designing Methods
1 Introduction
We start with designing data - designing classes of data that are connected to each other in a systematic way, showing that the Design recipe for Data Definitions can be used virually without change in a completely different language than we have used in the first part.
We start designing methods in the functional style, i.e., every method produces a new value (primitive or a new object) and makes no changes in the existing data. This allows us to use a very simple language, one where the only two statements are return and if (condition) then statement else statement.
The programs we provide give you examples of the progressively more complex data (class) definitions, and illustrate the design of methods for these class Hierarchies.
The design of methods follows the same Design Recipe we have seen before. The only difference here is that for classes that represent a union type (for example classes Circle and Rectangle that are both a part of the union type Shape, the conditional statement that would be used in some programming languages is replaced by the dynamic dispatch into the class whose constructor has been used to define the specific object.
2 The Design Recipe
The DESIGN RECIPE teaches us to design each method in six steps:
Step 1: Problem analysis and data definition.
Decides what information is needed and what information will the method produce. Design the data representatin for all needed information. Make examples of the data the method may consume and the data method may produce.
Step 2: The purpose statement and the header.
Design the method header and write a concise purpose statement. The purpose statement should refer to the object that invokes the method as this ... and refer to any instances the method consumes as arguments with the word given...
Note: For methods with side effects you also will need the EFFECT statement.
Step 3: Examples with expected outcomes.
Use the examples of data from before (with additional examples as needed to show how the method would be invoked in the client code and for each such sample invocation provide the values you expect the method to produce.
Note: For methods with side effects you also will need to show how you would evaluate the expected EFFECTs.
Step 4: The template.
The template for each methods lists all data available during the method’s execution: the fields in this class, the methods defined in this class, the methods that any of the fields in this class can invoke, as well as any method that may be invoked by the given data. If one or more of the arguments to the method is an instance of this class, include also all fields for this argument, as well as all methods these fields may invoke.
For each entry in the template specify the type of the value it represents.
Step 5: The method body.
Using the data collected in the Template step, and looking at the examples of the method invocation as well as the expected outcomes, design the method body.
Step 4: The tests.
Convert the examples from Step 3 into tests. Add any other tests you may need, based on the actual implementation of your method. Run the tests, if any of them fail fix the errors and run the tests again.
3 Methods for Simple Classes
Below is an example of the design of a method that computes the number of pixels in a photo image:
Step 1: Problem analysis and data definition.
The method deals with Photos and so it needs to be defined in the class Photo. Each instance of a Photo has all the information we need to solve the problem - we do not need any additional data to be given. The result is an integer.
We will use the following data in our examples. For your work you should add at least one more instance of each class.
// Examples for the class ClockTime
ClockTime ct1 = new ClockTime(21, 50);
ClockTime ct2 = new ClockTime(11, 30);
ClockTime ct3 = new ClockTime(9, 50);
// Examples for the class Date
Date d1 = new Date(2007, 9, 23);
Date d2 = new Date(2007, 11, 7);
Date d3 = new Date(2007, 9, 25);
// Examples for the class Photo
Photo river = new Photo("River", "jpeg", 3456, 2304,
3614571, this.d1, this.ct3);
Photo mountain = new Photo("Mountain", "jpeg", 2448, 3264,
1276114, this.d2, this.ct2);
Photo people = new Photo("People", "gif", 545, 641,
13760, this.d2, this.ct1);
Photo icon = new Photo("PLTicon", "bmp", 16, 16,
1334, this.d1, this.ct2);
Step 2: The purpose statement and the header.
// to compute the number of pixels in this photo
int pixels(){...}
Step 3: Examples with expected outcomes.
people.pixels() ---> 349345
icon.pixels() ---> 256
Step 4: The template.
int pixels(){
... this.name ... --- String
... this.kind ... --- String
... this.width ... --- int
... this.height ... --- int
... this.bytes ... --- int
... this.date ... --- Date
... this.time ... --- ClockTime
}
We will only need this.width and this.height.
Step 5: The method body.
// to compute the number of pixels in this photo
int pixels(){
return this.width * this.height;
}
Step 6: Tests.
Our tester provides a special way of running the tests. The Tester class defines a method checkExpect (and several similar methods) that consumes two pieces of data of any type:
the actual value, i.e. the test method invocation
expected test result
It produces the test result as a boolean value and all test results are reported in a separate display. The following code (defined in a new class named ExamplesPhotos (or any other name, but typically starting with Examples):
// Tests for the method pixels:
boolean testPixels(Tester t){
return
t.checkExpect(this.people.pixels(), 349345) &&
t.checkExpect(this.icon.pixels(), 256);
}
shows the test method that defines the tests for our method.
Note: We must add the line
import tester.*;
before any of the class definitions, so that our code can invoke the methods defined in the tester library. We can them run the tests by invoking the main method in the class tester.Main and providing as a comman line argument the name of the Examples class that defined the sample data and the test methods.
4 Methods for Self-Referential Data
Let us now consider the class hierarchy that represents a list of photo images. We first design the method that counts the images in our list. (We use the simpler version of the class Photo.
Note: Of course, you will quickly realize that this method will look the same regardless of what are the pieces of data contained in the list. We will address that issue later on, once we are comfortable with dealing with lists that contain specific items.
The class diagram for a list of photo images is shown below:
+--------------+ |
| IListOfPhoto |<----------------+ |
+--------------+ | |
+--------------+ | |
| | |
/ \ | |
--- | |
| | |
------------------------ | |
| | | |
+---------------+ +-------------------+ | |
| MTListOfPhoto | | ConsListOfPhoto | | |
+---------------+ +-------------------+ | |
+---------------+ +-| Photo first | | |
| | IListOfPhoto rest |----+ |
| +-------------------+ |
v |
+----------------+ |
| Photo | |
+----------------+ |
| String name | |
| String kind | |
| int width | |
| int height | |
| int bytes | |
+----------------+ |
Method design for self-referential data
Below is an example of the design of a method that counts the number of pictures in a list of photo images.
The method deals with IListOfPhotos. We have an interface
IListOfPhotos
and two classes that implement the interface:
MTListOfPhotos and ConsListOfPhotos. When the Design Recipe calls for the method purpose statement and the header, we include the purpose statement and the header in the interface IListOfPhotos and in all the classes that implement the interface.
Including the method header in the interface serves as a contract that requires that all classes that implement the interface define the method with this header. As the result, the method can be invoked by any instance of a class that implement the interface - without the need for us to distinguish what is the defined type of the object.
We can now proceed with the Design Recipe.
Step 1: Problem analysis and data definition.
The only piece of data needed to count the number of elements in a list is the list itself. The result is an integer.
We will use the following data in our examples. For your work add at least one more instance of each class.
// Examples for the class Photo
Photo river =
new Photo("River", "jpeg", 3456, 2304, 3614571);
Photo mountain =
new Photo("Mountain", "jpeg", 2448, 3264, 1276114);
Photo people =
new Photo("People", "gif", 545, 641, 13760);
Photo icon =
new Photo("PLTicon", "bmp", 16, 16, 1334);
IListOfPhotos mtlist = new MTListOfPhotos();
IListOfPhotos list1 =
new ConsListOfPhotos(this.river, this.mtlist);
IListOfPhotos list2 =
new ConsListOfPhotos(this.mountain,
new ConsListOfPhotos(this.people,
new ConsListOfPhotos(this.icon, this.mtlist)));
Step 2: The purpose statement and the header.
// to count the number of pictures in this list of photos
int count(){...}
In the interface IListOfPhotos we write:
// to count the number of pictures in this list of photos
int count();
indicating there is no definition for this method.
We now have to design the method separately for each of the two classes.
Step 3: Examples.
We make examples for the empty list, a list with one element and a longer list:
mtlist.count() ---> 0
list1.count() ---> 1
list2.count() ---> 3
Step 4: The template.
We need to look separately at the two classes that implement the method.
class MTListOfPhotos:
The class has no member data and there is no other data available. It is clear that the method will always produce the same result, the value 0.
We can finish the steps 4. and 5. right away —
the method body becomes: // to count the number of pictures in this list of photos
int count() {
return 0;
}
class ConsListOfPhotos:
The template for the class ConsListOfPhotos includes the two fields: this.first and this.rest. However, we recognize that this.rest is a data of the type IListOfPhotos and so it can invoke the method count that is now under development. The template then becomes:
In the class ConsListOfPhotos:
TEMPLATE:
int count(){
... this.first ... --- Photo
... this.rest ... --- IListOfPhotos
... this.rest.count() ... --- int
}
Recall the purpose statement for the method count:
// to count the number of pictures in this list of photos
The purpose of the method invocation this.rest.count() then becomes:
// to count the number of pictures
// in the rest of this list of photos
// ---------------------
When designing methods for self-referential data, make sure you say out loud (or at least understand clearly) the purpose statement as applied to the self-referential method invocation.
Step 5: The method body.
We have already finished the method body for the class MTListOfPhotos. In the class ConsListOfPhotos the method body is:
// to count the number of pictures in this list of photos
int count(){
return 1 + this.rest.count();
}
Step 6: Tests.
We can now convert our examples into tests:
// Tests for the method count:
boolean testCount(Tester t){
return
t.checkExpect(this.mtlist.count(), 0) &&
t.checkExpect(this.list1.count(), 1) &&
t.checkExpect(this.list2.count(), 3);
}