It is time again to reflect on our design recipe for methods (see sections 10.3 and 11.2). The five steps work well in principle, but clearly the organization of some classes as a union suggests additional checkpoints:
1. Formulate a purpose statement and a method signature to the inter- face; then add the method signature toeachimplementing class. 2. Illustrate the purpose statement with examples for each class of the
union, i.e., for each variant.
3. Lay out what you know about the method’s argument(s) in each con- crete method. This includes references to the fields of the class. Remember from section 11.2 that if any of the variants contain an instance of another class, you should place appropriate schematic method calls to methods in these other classes in the template. The purpose of these schematic calls is to remind you of the wish list dur- ing the next step. To this end, also add a tentative method header to that other class.
4. Define the methods. The parameters and the expressions in the tem- plate represent the information that may contribute to the result. Each schematic method call means that you may need to design an auxil- iary method in some other class. Use the wish list to keep track of these auxiliary goals.
5. Turn the examples into tests and run them.
As this section showed, you can switch steps 1 and 3. That is, you can de- velop the templates just based on the structure of the class hierarchy and the classes themselves. The example in this section also showed how help- ful the template-directed design is. Once you understand the template, the rest of the design task is often straightforward.
15
Methods and Classes with Mutual References
15.1 Example: Managing a Runner’s Logs. . . Develop a program that manages a runner’s training log. Every day the runner enters one entry concerning the day’s run. . . .
interfaceILog{
??? nnn();
}
classMTLogimplementsILog{
MTLog(){}
??? nnn(){ . . .
} }
classConsLogimplementsILog{ Entry fst;
ILog rst;
ConsLog(Entry fst,ILog rst){
. . . //omitted . . . } ??? nnn(){ . . . this.fst.mmm() . . . . . . this.rst.nnn() . . . } } classEntry{ Date d;
doubledistance;// miles
intduration;// minutes String comment; Entry(Date d, doubledistance, intduration, String comment){ . . . //omitted . . . } ??? mmm(){ . . . this.d.lll() . . . . . . this.distance. . . . . . this.duration. . . . . . this.comment. . . } } classDate{ intday; intmonth; intyear;
Date(intday,intmonth,intyear){
. . . //omitted . . . } ??? lll(){ . . . this.day. . . . . . this.month. . . . . . this.year. . . } }
Figure 51: Classes for a runner’s log
Figure 18 (page 38) displays the class diagram for a data representation of this problem. Figure 51 contains the matching class definitions. A log
is either empty, which we represent with an instance of MTLog, or it is constructed from anEntry and a log. An instance of Entry contains four fields: aDate d, adouble for thedistancein miles, an intfor thedurationin minutes, and aStringfor recording qualitative remarks.
In addition, figure 51 contains sketches of method templates: a signa- ture for a methodnnninILogand concrete templates forMTLog,ConsLog, Entry, and Date. Later we must refine these templates with return types and arguments.
The templates inMTLog,Entry, andDateare unremarkable; their design strictly follows the design recipe, especially the ones for unions and classes that refer to other classes. While this is also true for the template inConsLog, that template contains the expression
. . . this.rst.nnn() . . .
which refers to the methodnnn in ILog. Since rst is a reference to some other box in the class diagram, this basically follows from the design recipe for classes that contains instances of other classes. ILogisn’t an ordinary class, however, but an interface. Still, we know that if an interface specifies a method, then all implementing classes define this method. Therefore it is proper to referencennnin this context. Indeed, by doing so, the method template matches the class diagram in a perfect manner; the class hierarchy has determined the shape of the program one more time.
From section 13 we also know what this means for the evaluation of such expressions. The value inrstis going to be either an instance ofMTLog orConsLog. In each case, the method call is directed to the concrete method in the corresponding class. Ifrstis an instance ofConsLog, the method calls itself; otherwise it callsnnninMTLog.
Note: If you have experiencedHow to Design Programsproperly, you know not to look ahead. Just trust the design recipe and its claim that methods work according to their purpose statement, both other methods (i.e., those on the wish list) and the same method (recursively). If you haven’t worked throughHow to Design Programs, we need to ask you to trust us for the moment. Once you experience for yourself how smoothly the design works out, you will know why we asked you to just plunge ahead.
Let’s see whether working out the template first works as well for self- referential class diagrams as it did for unions. Here is a first problem:
. . . The runner will want to know the total number of miles run. . . .
classCompositeExamples{
Date d1=newDate(5,5,2003);
Date d2=newDate(6,6,2003);
Date d3=newDate(23,6,2003);
Entry e1=newEntry(d1,5.0,25,"Good");
Entry e2=newEntry(d2,3.0,24,"Tired");
Entry e3=newEntry(d3,26.0,156,"Great");
ILog l1=newMTLog();
ILog l2=newConsLog(e1,l1);
ILog l3=newConsLog(e2,l2);
ILog l4=newConsLog(e3,l3);
CompositeExamples(){ } }
Figure 52: Examples for a runner’s log
inside ofILog:
//to compute the total number of miles recorded inthislog doublemiles();
In addition, you can now rename the methods inMTLogandConsLog. For the functional examples, we use the sample objects from our orig- inal discussion of a runner’s log: see figure 52, where they are repeated. Invoking themilesmethod on theILogs produces the obvious results:
checkl1.miles()expect0.0within.1
checkl2.miles()expect5.0within.1
checkl3.miles()expect8.0within.1
checkl4.miles()expect34.0within.1
We use the examples to design each concrete method, case by case: 1. The examples show that inMTLogthe method just returns0.0. A log
with no entries represents zero miles.
2. The template forConsLogcontains two expressions. The first says that we can compute withfst, which is an instance ofEntry. The second one computesthis.rst.miles(). According to the purpose statement in ILog, this expression returns the number of miles in therstlog that is included inthisinstance ofConsLog. Put differently, we can just add the number of miles in thefst Entryto those fromrst.
Using this problem analysis, it is easy to write down the two methods: inside ofMTLog: doublemiles(){ return0; } inside ofConsLog: doublemiles(){
return this.fst.distance+this.rst.miles(); }
All that remains to be done is to test the methods with the examples. In light of our discussion on the differences between Scheme-based computations and a Java-based one, it is also important to see that this method looks very much like the addition function for lists:
(define(add-lists-of-numbers alon) (cond[(empty?alon)0]
[else(+(first alon) (add-lists-of-numbers rest alon))]))
The expression from the first conditional branch shows up inMTLogand the one from the second branch is in the method forConsLog, which is just as it should be. The conditional itself is invisible in the object-oriented pro- gram, just as described on page 13.1:
Often a runner doesn’t care about the entire log from the beginning of time but a small portion of it. So it is natural to expect an extension of our problem with a request like this:
. . . The runner will want to see his log for a specific month of his training season. . . .
Such a portion of a log is of course itself a log, because it is also a sequence of instances ofEntry.
Put differently, the additional method consumes a runner’s log and two integers, representing a month, and a year. It produces a runner’s log—of one month’s worth of entries:
inside ofILog:
//to extract those entries inthislog for the given month and year ILog oneMonth(intmonth,intyear);
checkl1.oneMonth(6,2003)expectl1
checkl3.oneMonth(6,2003)expect newConsLog(e2,MTLog) checkl3.oneMonth(6,2003)
expect newConsLog(e3,newConsLog(e2,MTLog()))
As before, the examples are based on the sample logs in figure 52. The first one says that extracting anything from an empty log produces an empty
log. The second one shows that extracting the June entries from l2gives us a log with exactly one entry. The last example confirms that we can get back a log with several entries.
The examples suggest a natural solution forMTLog. The design of the concrete method forConsLogrequires a look at the template to remind our- selves of what data is available. The second method call in the template,
this.rst.oneMonth(month,year)
produces the list of entries made in the given month and year extracted from the rest of the log. The other one,
this.fst.mmm(month,year)
deals withfst, i.e., instances ofEntry. Specifically it suggests that we may wish to design a separate method for computing some value about an in- stance ofEntry. In this case,Entryneeds a method that determines whether an instance belongs to some given month and year, because theoneMonth method should includefstonly if it belongs to the given month and year.
To avoid getting distracted, we add an entry on our wish list: inside ofEntry:
//wasthisentry made in the given month and year? booleansameMonthAndYear(intmonth,intyear){. . . }; But before we design this method, let’s finishoneMonthfirst.
Assuming thatoneMonthis designed properly and works as requested, we can finish the method inConsLogeasily:
inside ofMTLog:
ILog oneMonth(intm,inty){ return newMTLog(); }
inside ofConsLog:
ILog oneMonth(intm,inty){
if(this.fst.sameMonthAndYear(m,y)){ return new ConsLog( this.fst, this.rst.oneMonth(m,y));} else{
return this.rst.oneMonth(m,y);} }
The method inConsLogmust distinguish two possibilities. If this.fst.sameMonthAndYear(m,y)
isfalse, the result is whateveroneMonthextracted fromrst. If it istrue,fstis included in the result; specifically, the method creates a newConsLogfrom fstand whateveroneMonthextracts fromrst.
With the methods forMTLog andConsLog completed, we turn to our wish list. So far it has one item on it: sameMonthAndYear in Entry. Its method template (refined from figure 51) is:
inside ofEntry:
booleansameMonthAndYear(intmonth,intyear){
. . . this.d.lll() . . . this.distance. . . this.duration. . . this.comment. . . }
This implies that the method should calculate withmonth,year, andd, the Datein the givenEntry. The suggestive method callthis.d.lll() tells us that we can delegate all the work to an appropriate method inDate. Of course, this just means adding another item to the wish list:
inside ofDate:
//isthisdate in the given month and year?
booleansameMonthAndYear(intmonth,intyear){. . . }
Using “wishful thinking” gives us the full definition ofsameMonthAndYear: inside ofEntry:
booleansameMonthAndYear(intmonth,intyear){ return this.d.sameMonthAndYear(month,year);
The one thing left to do is to designsameMonthAndYearforDate. Natu- rally, we start with a refinement of its template:
inside ofDate:
booleansameMonthAndYear(intmonth,intyear){ . . . this.day. . . this.month. . . this.year. . . }
This template tells us that sameMonthAndYear has five numbers to work with: month, year, this.day, this.month, and this.year. Given the purpose statement, the goal is clear: the method must compare month with this. monthandyearwiththis.year:
inside ofDate:
//isthisdate in the given month and year? booleansameMonthAndYear(intmonth,intyear){
return(this.month==month)&&(this.year==year); }
This finishes our wish list and thus the development of oneMonth. You should realize that we again skipped making up examples for the two “wishes.” While this is on occasion acceptable when you have a lot of ex- perience, we recommend that you develop and test such examples now. Exercises
Exercise 15.1 Collect all the pieces ofoneMonthand insert the method defi- nitions in the class hierarchy for logs. Develop examples and include them with the test suite. Draw the class diagram for this hierarchy (by hand). Exercise 15.2 Suppose the requirements for the program that tracks a run- ner’s log includes this request:
. . . The runner wants to know the total distance run in a given month. . . .
Design the method that computes this number and add it to the class hier- archy of exercise 15.1.
Consider designing two different versions. The first should follow the design recipe without prior considerations. The second should take into account that methods can compose existing methods and that this partic- ular task can be seen as consisting of two separate tasks. (The design of each method should still follow the regular design recipe.) Where would you put the second kind of method definition in this case? (See the next chapter.)
Exercise 15.3 Suppose the requirements for the program that tracks a run- ner’s log includes this request:
. . . A runner wishes to know the length of his longest run ever. [He may eventually wish to restrict this inquiry into a particular season or runs between two dates.] . . .
Design the method that computes this number and add it to the class hier- archy. Assume that the method produces0if the log is empty.
Also consider this variation of the problem:
. . . A runner wishes to know whether all distances are shorter than some number of miles. . . .