Take a look at this revised version of our very first problem:
. . . Design a method that computes the cost of selling bulk coffee at a specialty coffee seller from a receipt that includes the kind of coffee, the unit price, and the total amount (weight) sold. . . .
In section 2, we designed theCoffeeclass to represent the information about a coffee sale. It is now time to design the method that actually computes the cost of such a transaction.
Instead of plunging directly into the programming activity, let us recall how we would design this function using the recipes fromHow to Design Programs. The first step of our generic design recipe calls for the design of a data representation; we already have that:
(define-structcoffee(kind price weight)) ;;Coffee(sale) is:
;; — (make-coffee String Number Number)
The goal of the second step is to write down a contract, concise purpose statement, and a function header:
;;cost : Coffee → String
;; to compute the total cost of a coffee purchase (define(cost a-coffee) . . . )
Herea-coffeeis the function parameter that stands for the instance ofcoffee structure supplied with applications ofcost.
Next we must make up some examples, that is, function calls for cost that illustrate what it should produce when given certain arguments. Nat- urally, we use the examples from the problem statement (page 9):
(cost(make-coffee"Kona"2095 100)) ; should produce 209500
Turn the other two data examples into functional examples, too.
The fourth step is the crucial one for most function designs. It requests that we refine the function header to a function template by making all the knowledge about the structure of the arguments explicit. After all, the function computes the outputs from the given information. Sincecostcon- sumes a structure—an instance ofcoffee—you have the structure itself and all of its field values:
(define(cost a-coffee)
. . . (coffee-kind a-coffee) . . . . . . (coffee-price a-coffee) . . . . . . (coffee-weight a-coffee) . . . )
The template contains three (selector) expressions, because thecoffeestruc- ture has three fields. Each expression extracts one value froma-coffee, the parameter.
The transition from the template to the full function definition—the fifth step—starts with an examination of the data that the function consumes. The function must be able to compute the result from just these pieces of data. Here we need only two of the pieces, of course:
(define(cost a-coffee)
(∗(coffee-price a-coffee) (coffee-weight a-coffee)))
The sixth and final step is to test the examples that we worked out above. In Java, we don’t design independent functions. As we already know from section 9, we instead design methods that are a part of a class. Later we invoke the method on an instance of this class, and this instance is the method’s primary argument. Thus, if the Coffee class already had a cost method, we could write in the example section
newCoffee("Kona",2095,100).cost()
and expect this method call to produce209500.
Let’s try to develop this method systematically, following our well- known design recipe. First we add a contract, a purpose statement, and a header forcostto theCoffeeclass:
//the bill for a coffee sale classCoffee{
String kind;
intprice;// in cents per pound intweight;// in pounds
Coffee(String kind,intprice,intweight)//intentionally omitted //to compute the total cost of this coffee purchase [in cents] intcost(){. . . }
}
The purpose statement for the method is a comment just like the purpose statement for the class. The contract is no longer a comment, however. It is an integral part of a Java method. In the terminology of Java, it is a METHOD SIGNATURE. The int to the left ofcost says that we expect the method to produce an integer; the purpose statement reminds us that it is the number of cents.
At first glance, the signature also seems to say that cost doesn’t con- sume anything, but remember thatcostis always invoked on some specific instance of Coffee (and one could say it consumes an instance of Coffee). Furthermore, this instance is the primary argument to the method, and it therefore has a standard (parameter) name,this, which you never need to include in the parameter list explicitly. We can thus use this in the pur- pose statement—reminding readers of the role of the special argument— and method body to refer to the instance ofCoffeeon whichcostis invoked:
inside ofCoffee:
//to compute the total cost ofthiscoffee purchase [in cents] intcost(){. . . this. . . }
Note: To avoid wasting space, we show only the modified parts of a class. The underlined phrase is there to remind you where the fragment belongs.
Now that we have clarified the basic nature of the method, let’s refor- mulate the functional examples in Java:
ProfessorJ: Examples & Tests Coffee c=newCoffee("Kona",2095,100)
. . .
checkc.cost()expect209500
That is, in the context of an existing example, we invoke thecostmethod. Reformulate the other examples from the Scheme approach in Java.
The next step is to formulate the template. Recall that the template ex- presses what we know about the argument(s). In our running example, the
“input” isthisinstance of the class. Each instance consists of three pieces of data: thekind, theprice, and theweight. In Scheme, we use special func- tions, the selectors, to extract the relevant pieces. In Java, we access an object’s fields with the dot notation. In general, we write:
object.field
Since we wish to access the fields ofthis object, we writethis.kind, this. price, andthis.weightin the method body to create the template:
inside ofCoffee:
//to compute the total cost ofthiscoffee purchase intcost(){
. . . this.kind. . . this.price. . . this.weight. . . }
The rest is easy. We must decide which pieces of the template are rele- vant and how to use them. In our running example, the two relevant pieces arethis.priceandthis.weight. If we multiply them, we get the result that we want:
inside ofCoffee:
//to compute the total cost ofthiscoffee purchase intcost(){
return this.price∗this.weight; }
Thereturnkeyword points the reader to the expression in a method body that produces the result of a method call. While it is obvious here because there is just one way to compute the result, you may already be able to imagine that such a hint can be useful for conditional expressions. The complete class definition including the method is shown in figure 38.
The figure also contains an extended class diagram. In this new dia- gram, the box forCoffeeconsists of three compartments. The first still names the class, and the second still lists the fields that each instance has. The third and new compartment is reserved for the method signatures. If the method’s name doesn’t sufficiently explain the computation of the method, we can also add the purpose statement to the box so that the diagram can tell the story of the class by itself.
ProfessorJ:
Automatic Tests Finally, the figure extends the examples class from chapter I (page 15).
In addition to the sample instances ofCoffee, it also contains threetestfields: testKona,testEthi, andtestColo. Like the example fields, these test fields are also initialized. The right-hand side of their initialization “equation” is a check. . . expect. . . expression, which compares the result of a method call
//represent a coffee sale:
//at which price, how much
// coffee was sold
classCoffee{ String kind; intprice; intweight;
Coffee(String kind, intprice, intweight){
this.kind=kind;
this.price=price;
this.weight=weight;
}
//to compute the total cost
//ofthiscoffee purchase intcost(){
return this.price∗this.weight;
} }
Coffee Stringkind
int price [in cents per pound] intweight [in pounds] intcost()
//collect examples of coffee sales
classCoffeeExamples{
Coffee kona=
newCoffee("Kona",2095,100);
Coffee ethi=
newCoffee("Ethiopian",800,1000);
Coffee colo=
newCoffee("Colombian",950,20);
booleantestKona=
check this.kona.cost()expect209500;
booleantestEthi=
check this.ethi.cost()expect800000;
booleantestColo=
check this.colo.cost()expect19000;
CoffeeExamples(){ }
}
Figure 38: Thecostmethod for theCoffeeclass
with an expected value and producestrueorfalse. When you place the two classes into the definitions window and run the program, ProfessorJ creates an instance ofCoffeeExamplesand determines how many of the fields with testin their name aretrue.
Some methods must consume more data than justthis. Let us see how the design recipe applies to such problems:
. . . The coffee shop owner may wish to find out whether a coffee sale involved a price over a certain amount. . . .
Clearly, a method that can answer this question about any given instance of coffee must consume a second argument, namely, the number of cents with which it is to compare thepriceof the sale’s record.
inside ofCoffee:
//to determine whetherthiscoffee’s price is more thanamt booleanmoreCents(intamt){. . . }
The purpose statement again reminds us thatmoreCentsconsumes two ar- guments:thisandamt. Second, we make a couple of examples:
check newCoffee("Kona",2095,100).moreCents(1200)expecttrue
check newCoffee("Columbian",950,200).moreCents(1000)expectfalse To practice your design skills, explain the expected results.
The template for this method is exactly that forcost: inside ofCoffee:
//to determine whetherthiscoffee’s price is more thanamt booleanmoreCents(intamt){
. . . this.kind. . . this.price. . . this.weight }
We do not have to include anything about the second argument,amt, be- cause it is a part of the signature and its type is justint, i.e., atomic data.
The only relevant pieces of data in the template areamtandthis.price: inside ofCoffee:
//to determine whetherthiscoffee’s price is more thanamt booleanmoreCents(intamt){
return this.price>amt; }
Don’t forget that the last step of the design recipe is to run the examples and to check that the method produces the expected results. So, turn the examples into additionaltestfields inCoffeeExamples(from figure 38).
Lastly, methods may also have to consume instances of classes, not just primitive values, as their secondary arguments. Take a look at this problem:
. . . The coffee shop owner may also wish to find out whether some coffee sale involved more weight than some given coffee sale. . . .
Naturally, a method that compares the weight for two kinds of coffee con- sumes two instances ofCoffee. We call themthisand, by convention,thatin the purpose statement:
inside ofCoffee:
//to determine whetherthiscoffee sale is lighter thanthatcoffee sale booleanlighterThan(Coffee that){. . . }
Let’s make up some examples:
check newCoffee("Kona",2095,100)
.lighterThan(newCoffee("Columbian",950,200) )expecttrue
check newCoffee("Ethiopian",800,1000)
.lighterThan(newCoffee("Columbian",950,200) )expectfalse
The gray-shaded boxes representthat, the second argument tolighterThan. For both cases, the answers are obvious because in order to determine whether one coffee sale involves less weight than the other, we just com- pare the weights in each.
In the template for a method such as lighterThan, we write down the fields for this and the fields for the instance of the other argument. Re- member that that.kind extracts the value from thekind field of the object that:
inside ofCoffee:
//to determine whetherthiscoffee sale is lighter thanthatcoffee sale booleanlighterThan(Coffee that){
. . . this.kind. . . that.kind. . . // String . . . this.price. . . that.price. . . //int . . . this.weight. . . that.weight. . . //int }
Note how we have added comments about the types of the fields in this template. Adding type annotations is useful when you move from the tem- plate step to the method definition step.
Of course, the only relevant fields are theweight fields, and so we get this complete and simple definition:
inside ofCoffee:
//to determine whetherthiscoffee sale is lighter thanthatcoffee sale booleanlighterThan(Coffee that){
return this.weight< that.weight; }
Now test the method to validate the examples that we made up. The com- plete code for theCoffeeclass is displayed in figure 39.