How To Design Classes
How To Design Classes
Matthias Felleisen
Matthew Flatt
Robert Bruce Findler
Kathryn E. Gray
Shriram Krishnamurthi
Viera K. Proulx
c
2003,
2004, 2005, 2006, 2007, 2008 Felleisen, Flatt, Findler, Gray,
Krishnamurthi, Proulx
How to design class: object-oriented programming and computing
Matthias Felleisen, Matthew Flatt, Robert Bruce Findler, Kathryn E. Gray,
Shriram Krishnamurthi, Viera K. Proulx
p. cm.
Includes index.
ISBN 0-262-06218-6 (hc.: alk. paper)
1. Computer Programming. 2. Electronic data processing.
QA76.6 .H697 2001
005.12dc21
00-048169
CONTENTS
Contents
Preface
xii
Acknowledgements . . . . . . . . . . . . . . . . . . . . . . . . . . . xiii
2 Classes
2.1 Finger Exercises on Plain Classes . . . . . . . . . . . . . . . .
2.2 Designing Classes . . . . . . . . . . . . . . . . . . . . . . . . .
9
15
18
19
25
26
4 Unions of Classes
4.1 Types vs Classes . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Finger Exercises on Unions . . . . . . . . . . . . . . . . . . .
4.3 Designing Unions of Classes . . . . . . . . . . . . . . . . . . .
27
30
32
35
37
37
44
49
50
54
56
64
II Functional Methods
64
67
69
83
CONTENTS
vi
83
85
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
87
. 87
. 93
. 95
. 96
. 104
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
142
142
150
153
163
171
16 Designing Methods
173
16.1 The Varieties of Templates . . . . . . . . . . . . . . . . . . . . 174
16.2 Wish Lists . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178
16.3 Case Study: Fighting UFOs, with Methods . . . . . . . . . . 179
CONTENTS
vii
Intermezzo 2: Methods
194
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
195
197
202
212
221
18 Similarities in Classes
18.1 Common Fields, Superclasses . . . . . .
18.2 Abstract Classes, Abstract Methods . . .
18.3 Lifting Methods, Inheriting Methods . .
18.4 Creating a Superclass, Creating a Union
18.5 Deriving Subclasses . . . . . . . . . . . .
.
.
.
.
.
222
222
226
228
234
250
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
251
252
255
258
261
265
267
272
277
287
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
291
292
296
298
304
305
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
viii
322
327
23 Circular Data
23.1 Designing Classes for Circular Objects, Constructors
23.2 The True Nature of Constructors . . . . . . . . . . .
23.3 Circularity and Encapsulation . . . . . . . . . . . . .
23.4 Example: Family Trees . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
328
337
340
340
343
348
359
361
362
364
367
373
375
378
384
387
389
395
399
408
418
420
423
28 Equality
28.1 Extensional Equality, Part 2 . .
28.2 Intensional Equality . . . . . . .
28.3 Extensional Equality with null
28.4 Extensional Equality with Cast
434
435
435
440
442
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
CONTENTS
ix
Intermezzo 4: Assignments
449
452
.
.
.
.
464
465
471
476
479
.
.
.
.
.
484
488
490
500
503
508
511
511
517
520
526
528
531
535
540
544
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
548
CONTENTS
551
.
.
.
.
.
.
.
.
.
.
.
.
551
553
554
556
561
569
575
.
.
.
.
.
.
.
.
.
587
587
590
591
594
596
608
610
613
617
619
619
621
626
631
635
642
643
645
645
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
42 Loops
645
42.1 Designing Loops . . . . . . . . . . . . . . . . . . . . . . . . . 645
42.2 Designing Nested Loops . . . . . . . . . . . . . . . . . . . . . 645
CONTENTS
xi
646
44 ArrayLists
646
Intermezzo 7: Loops
647
VIII Java
649
649
649
649
Preface
languages such as C# and Java support this effort with syntactic constructs.
We also refine the program design discipline.
Why Java?
Why ProfessorJ?
ProfessorJ is not Java; it is a collection of relatively small, object-oriented
programming languages made up for teaching the design of classes, i.e.,
the essence of object-oriented programming. For the most part, they are
subsets of Java but, to support the pedagogy of this book, they also come
with constructs for playing with examples and testing methods.
ProfessorJ is useful for the first four chapters. After that, it is essential
that you switch to a full-fledged programming language and an industrial
programming environment. This may mean switching to something like
Java with Eclipse or C# with Microsofts Visual Studio.
Acknowledgments
Daniel P. Friedman, for asking the first author to co-author A Little Java,
A Few Patterns (also MIT Press);
Richard Cobbe for many nagging questions on Java
typos: David van Horn
PICTURE:
Preface
TODO
add examples that initialize fields between sections 1 and LAST
add exercises that ask students to represent something with classes
that isnt completely representable; they need to recognize what to omit
when going from information to data. do it early in the chapter.
add modeling exercise to Intermezzo 1 that guides students through
the process of modeling Java syntax (information) via Java classes (data)
start with an exercise that says
class ClassRep String name; ClassRep(String name) this. name = name;
and ask them to translate a class with fields and one without.
In How to Design Programs, we learned that the systematic design of a program requires a solid understanding of the problem information. The first
step of the design process is therefore a thorough reading of the problem
statement, with the goal of identifying the information that the requested
program is given and the information that it is to compute. The next step is
to represent this information as data in the chosen programming language.
More precisely, the programmer must describe the classes of all possible
input data and all output data for the program. Then, and only then, it is
time to design the program itself.
Thus, when you encounter a new programming language, your first
goal is to find out how to represent information in this language. In How to
Design Programs you used an informal mixture of English and Scheme constructors. This book introduces one of the currently popular alternatives:
programming in languages with a notation for describing classes of data
within the program. Here the word class is short for collection in the
spirit in which we used the word in How to Design Programs; the difference
is that the designers of such languages have replaced data with object,
and that is why these languages are dubbed object-oriented.
Of course, for most problems describing a single class isnt enough. Instead, you will describe many classes, and you will write down how the
classes are related. Conversely, if you encounter a bunch of related classes,
you must know how to interpret them in the problem space. Doing all
this takes practiceespecially when data descriptions are no longer informal comments but parts of the programand that is why we dedicate the
entire chapter to the goal of designing classes and describing their relationships. For motivational previews, you may occasionally want to take a
peek at corresponding sections of chapter II, which will introduce functions
for similar problems and classes as the following sections.
ProfessorJ:
Beginner
Section 1
Like Scheme, Java provides a number of built-in atomic forms of data with
which we represent primitive forms of information. Here we use four of
them: int, double, boolean, and String.1
How to Design Programs uses number to represent numbers in the problem domain. For integers and rational numbers (fractions or numbers with
a decimal point), these representations are exact. When we use Javaand
most other programming languageswe give up even that much precision. For example, while Javas int is short for integer, it doesnt truly include all integersnot even up to the size of a computer. Instead, it means
the numbers
from 2147483648 to 2147483647 .
If an addition of two ints produces something that exceeds 2147483647,
Java finds a good enough2 number in the specified range to represent the result. Still, for our purposes, int is often reasonable for representing integers.
In addition to exact integers, fractions, and decimals, How to Design Programs also introduced inexact numbers. For example, the square root function (sqrt) produces an inexact number when given 2. For Java and similar
languages, double is a discrete collection of rational numbers but is used
to represent the real numbers. That is, it is roughly like a large portion of
the real number line but with large gaps. If some computation with real
numbers produces a number that is in the gap between two doubles, Java
somehow determines which of the two is a good enough approximation to
the result and takes it. For that reason, computations with doubles are inherently inaccurate, but again, for our purposes we can think of doubles as
a strange form of real numbers. Over time, you will learn when to use ints
and when to use doubles to represent numeric information.
As always, the boolean values are true and false. We use them to represent on/off information, absence/presence information, and so on.
Finally, we use Strings to represent symbolic information in Java. Symbolic information means the names of people, street addresses, pieces of
conversations, and similarly symbolic information. For now, a String is a
sequence of keyboard characters enclosed in quotation marks; e.g.,
1 For
Java, String values are really quite different from integers or booleans. We ignore
the difference for now.
2 Java uses modular arithmetic; other languages have similar conventions.
Classes
"bob"
"#$%&"
"Hello World"
"How are U?"
"It is 2 good to B true."
Naturally, a string may not include a quotation mark, though there are
ways to produce Strings that contain this character, too.
2 Classes
For many programming problems, we need more than atomic forms of data
to represent the relevant information. Consider the following problem:
. . . Develop a program that keeps track of coffee sales at a specialty coffee seller. The sales receipt must include the kind of
coffee, its price (per pound), and its weight (in pounds). . . .
The program may have to deal with hundreds and thousands of sales. Unless a programmer keeps all the information about a coffee sale together in
one place, it is easy to lose track of the various pieces. More generally, there
are many occasions when a programmer must represent several pieces of
information that always go together.
Our sample problem suggests that the information for a coffee sale consists of three (relevant) pieces: the kind of coffee, its price, and its weight.
For example, the seller may have sold
1. 100 pounds of Hawaiian Kona at $20.95/pound;
2. 1,000 pounds of Ethiopian coffee at $8.00/pound; and
3. 1,700 pounds of Colombian Supreme at $9.50/pound.
In How to Design Programs, we would have used a class of structures to
represent such coffee sales:
(define-struct coffee (kind price weight))
;; Coffee (sale) is:
;; (make-coffee String Number Number)
The first line defines the shape of the structure and operations for creating
and manipulating structures. Specifically, the definition says that a coffee
structure has three fields: kind, price, and weight. Also, the constructor is
called make-coffee and to get the values of the three fields, we can use the
Section 2
10
you wish to design programs that deal with numbers properly, you must study the
principles of numerical computing.
Classes
11
Coffee
String kind
int price [in cents per pound]
int weight [in pounds]
they are not really equations, they just look like that.
Section 2
12
Classes
13
// calendar dates
class Date {
int day;
int month;
int year;
Date
int day
int month
int year
Section 2
14
// GPS locations
class GPSLocation {
double latitude; // degrees ;
double longitude; // degrees
Location
double lattitude [in degrees]
double longitude [in degrees]
A class for GPS locations needs two fields: one for latitude and one for
longitude. Since both are decimal numbers that are approximate anyway,
we use doubles to represent them. Figure 3 contains both the class diagram
and the class definition. As for turning data into information (and vice
versa) in this context, see exercise 2.1.
// moving balls on a pool table
class Ball {
int x;
int y;
int RADIUS = 5;
Ball
int x
int y
int RADIUS
Ball(int x, int y) {
this.x = x;
this.y = y;
}
Classes
15
Now the radius of these balls is going to stay the same throughout the
simulation, while their coordinates constantly change. To express this distinction and to simplify the creation of instances of Ball, a programmer adds
an initialization equation directly to a field declaration: see RADIUS in
figure 4. By convention, we use uppercase letters for the names of fields to
indicate that they are constant, shared attributes.
If a field comes with an initialization equation, the constructor does
not contain an equation for that field. Thus, the Ball constructor consumes
two values, x and y, and contains two equations: one for x and one for
y. To create instances of Ball, you can now write expressions such as new
Ball(10,20), which produces an object with three attributes: x with value
10; y with value 20; and RADIUS with value 5. Explore the creation of such
objects in the interactions window.
// collect examples of coffee sales
class CoffeeExamples {
Coffee kona = new Coffee("Kona",2095,100);
Coffee ethi = new Coffee("Ethiopian", 800, 1000);
Coffee colo = new Coffee("Colombian", 950, 20);
CoffeeExamples() { }
ProfessorJ:
Examples
Section 2
16
Classes
17
Automobile
String model
int price [in dollars]
double mileage [in miles per gallon]
boolean used
Exercise 2.4 Translate the class diagram in figure 6 into a class definition.
Also create instances of the class.
Exercise 2.5 Create three instances of the following class:
// introducing the concept of gravity
class Apple {
int x;
int y;
int RADIUS = 5;
int G = 10; // meters per second square
Apple(int x, int y) {
this.x = x;
this.y = y;
}
How many attributes describe each instance? How many arguments does
the constructor consume?
Section 2
18
19
5.3 miles
2.8 miles
26.2 miles
...
27 minutes
24 minutes
150 minutes
...
feeling good
feeling tired
feeling exhausted
...
The three recordings are from three distinct dates, with widely varying
mileage and post-practice feelings.
If we were to represent these forms of data in Scheme we would formulate two structure definitions and two data definitions:
(define-struct entry (date distance duration comment))
;; Entry is:
;; (make-entry Date Number Number String)
(define-struct date (day month year))
;; Date is:
;; (make-date Number Number Number)
The first pair specifies the class of Entrys, the second the class of Dates. Just
as our analysis of the problem statement says, the data definition for Entry
refers to the data definition for Dates.
Section 3
20
Entry
Date d
double distance [in miles]
int duration [in minutes]
String comment
String comment) {
this.d = d;
this.distance = distance;
this.duration = duration;
this.comment = comment;
Date
int day
int month
int year
// calendar dates
class Date {
int day;
int month;
int year;
21
ProfessorJ:
More on Examples
Section 3
22
Restaurant
String name
String kind
String pricing
Place place
// a restaurant in Manhattan
class Restaurant {
String name;
String kind;
String pricing;
Place place;
Place
int ave
int street
Restaurant(String name,
String kind,
String pricing,
Place place) {
this.name = name;
this.kind = kind;
this.pricing = pricing;
this.place = place;
}
// a intersection in Manhattan
class Place {
int ave;
int street;
. . . Develop a program that helps a visitor navigate Manhattans restaurant scene. The program must be able to provide
four pieces of information for each restaurant: its name, the
kind of food it serves, its price range, and the closest intersection (street and avenue).
Examples: (1) La Crepe, a French restaurant, on 7th Ave and
65th Street, moderate prices; (2) Bremen Haus, a German restaurant on 2nd Ave and 86th Street, moderate; (3) Moon Palace, a
Chinese restaurant on 10th Ave and 113th Street, inexpensive;
...
23
Section 3
24
Route
String origin
String destination
Train
Route r
Schedule s
boolean local
Schedule
ClockTime
ClockTime departure
ClockTime arrival
int hour
int minute
// a train route
class Route {
String origin;
String destination;
// a train schedule
class Schedule {
ClockTime departure;
ClockTime arrival;
Schedule(ClockTime departure,
ClockTime arrival) {
this.departure = departure;
this.arrival = arrival;
}
25
is a short step to an equivalent class definition; see figure 10 for the four
definitions.
Lets look at some examples:
Route r1 = new Route("New York", "Boston");
Route r2 = new Route("Chicago", "New York");
ClockTime t1 = new ClockTime(23, 50);
ClockTime t2 = new ClockTime(13, 20);
ClockTime t3 = new ClockTime(10, 34);
ClockTime t4 = new ClockTime(13, 18);
Schedule s1 = new Schedule(t1,t2);
Schedule s2 = new Schedule(t3,t4);
Train train1 = new Train(r1, s1, true);
Train train2 = new Train(r2, s2, false);
This collection of definitions introduces two trains, two schedules, four
clock times, and two routes. Interpret these objects in the context of the
original problem and determine whether someone can reach Chicago from
Boston in a day according to this imaginary train schedule.
Section 3
26
WeatherRecord
Date d
TemperatureRange today
TemperatureRange normal
TemperatureRange record
Date
TemperatureRange
int day
int month
int year
int high
int low
Exercise 3.3 Revise the data representation for the book store assistant in
exercise 2.2 so that the program can find additional information about authors (name, year of birth). Modify the class diagram, the class definition,
and the examples.
Unions of Classes
27
primitive classes when we make up examples. As in the advice for designing basic classes, it is important to transform information into data and to
understand how data represents information. If things look complex, dont
forget the advice from How to Design Programs on creating data representations via an iterative refinement of more and more complex diagrams.
4 Unions of Classes
Our railroad example in the preceding section distinguishes between two
kinds of trains with a boolean field. If the field is true, the instance represents a local train; otherwise, it is an express train. While a boolean field
may work for a simple distinction like that, it really isnt a good way to
think about distinct kinds of trains. It also doesnt scale to large problems,
like those of real train companies, which offer a wide variety of trains with
many distinct attributes, e.g., city, local, regional, long distance, long distance express trains, and so on.
In this section, we show how to use classes to represent distinct yet
related kinds of information, such as kinds of trains. Even though the train
problem would benefit from this reformulation, we use a new problem and
leave the train example to an exercise:
. . . Develop a drawing program that deals with three kinds of
shapes on a Cartesian grid: squares, circles, and dots.
(20,50)
6
30
'$
20@
&%
(0,0)
t
@
(100,200)
Section 4
28
IShape
Dot
Square
Circle
CartPt loc
CartPt loc
int size
CartPt loc
int radius
CartPt
int x
int y
Unions of Classes
29
for the last three is easy; for Shape we just use an empty box for now. Since
the relationship between Shape, on one hand, and the Dot, Square, and Circle, on the other, differs from anything we have seen, however, we actually
need to introduce two new concepts before we can proceed.
// geometric shapes
interface IShape {}
// a dot shape
class Dot
implements IShape {
CartPt loc;
Dot(CartPt loc) {
this.loc = loc;
}
}
// a square shape
class Square
implements IShape {
CartPt loc;
int size;
Square(CartPt loc,
int size) {
this.loc = loc;
this.size = size;
}
// a circle shape
class Circle
implements IShape {
CartPt loc;
int radius;
Circle(CartPt loc,
int radius) {
this.loc = loc;
this.radius = radius;
}
CartPt(int x, int y) {
this.x = x;
this.y = y;
}
Section 4
30
Figure 13 displays the complete set of interface and class definitions for
figure 12. Once you accept the two syntactic novelties, translating class
diagrams into text is as mechanical as before.
Unions of Classes
31
Exercise
Exercise 4.1 Translate the three graphical shape examples from the problem statement into objects in the context of figure 13. Conversely, sketch
the following instances on a grid: new Dot(new CartPt(3,4)); new Circle(new CartPt(12,5),10); and new Square(new CartPt(30,60),20).
Exercise 4.2 Consider this Java rendition of a union:
interface ISalesItem {}
class DeepDiscount implements ISalesItem {
int originalPrice;
...
}
class RegularDiscount implements ISalesItem {
int originalPrice;
int discountPercentage;
...
}
Say, in this context, you encounter these examples:
ISalesItem s = new DeepDiscount(9900);
ISalesItem t = new RegularDiscount(9900,10);
RegularDiscount u = new RegularDiscount(9900,10);
What are the types of s, t, and u? Also, someone has written down the
following examples:
RegularDiscount v = new DeepDiscount(9900);
DeepDiscount w = new RegularDiscount(9900,10);
RegularDiscount x = new RegularDiscount(9900,10);
Which of them are type correct and which one are type errors?
Section 4
32
IZooAnimal
Lion
Snake
Monkey
int meat
String name
int weight
int length
String name
int weight
String food
String name
int weight
Unions of Classes
33
// zoo animals
interface IZooAnimal{}
// a zoo lion
class Lion
implements
IZooAnimal{
int meat;
String name;
int weight;
Lion(String name,
int weight,
int meat){
this.name = name;
this.weight = weight;
this.meat = meat;
}
// a zoo snake
class Snake
implements
IZooAnimal{
int length;
String name;
int weight;
Snake(String name,
int weight,
int length){
this.name = name;
this.weight = weight;
this.length = length;
}
// a zoo monkey
class Monkey
implements
IZooAnimal{
String food;
String name;
int weight;
Monkey(String name,
int weight,
String food){
this.name = name;
this.weight = weight;
this.food = food;
}
Exercises
Exercise 4.3 Modify the representation of trains in figure 10 so that local
and express trains are separate classes.
Section 4
34
ITaxiVehicle
Cab
Limo
Van
int idNum
int passengers
int pricePerMile
int minRental
int idNum
int passengers
int pricePerMile
boolean access
int idNum
int passengers
int pricePerMile
Unions of Classes
35
All have names for source files and sizes (number of bytes).
Images also include information about the height, the width,
and the quality of the image. Texts specify the number of lines
needed for visual representation. Sounds include information
about the playing time of the recording, given in seconds. . . .
Develop a data representation for these media. Then represent these three
examples with objects:
1. an image, stored in flower.gif; size: 57,234 bytes; width: 100 pixels; height: 50 pixels; quality: medium;
2. a text, stored in welcome.txt; size: 5,312 bytes; 830 lines;
3. a music piece, stored in theme.mp3; size: 40,960 bytes, playing time
3 minutes and 20 seconds.
Exercise 4.6 Take a look at the class diagram in figure 16. Translate it into
interface and class definitions. Also create instances of each class.
Exercise 4.7 Draw a diagram for the classes in figure 17 (by hand).
Section 4
36
Admission(Date d,
int price) {
this.d = d;
this.price = price;
}
for ClockTime,
see figure 10
// omnimax admission
class OmniMax
implements ITicket {
Date d;
int price;
ClockTime t;
String title;
// laser admission
class LaserShow
implements ITicket {
Date d;
int price;
ClockTime t;
String row;
int seat;
OmniMax(Date d,
int price,
ClockTime t,
String title) {
this.d = d;
this.price = price;
this.t = t;
this.title = title;
}
LaserShow(Date d,
int price,
ClockTime t,
String row,
int seat) {
this.d = d;
this.price = price;
this.t = t;
this.row = row;
this.seat = seat;
}
and things begin to look complicated, it is important to focus on the design of the union diagram first and to add the containment portion later,
following the suggestions in section 3.2.
After we have a complete class diagram, we translate the class diagram
into classes and interfaces. The box for the interface becomes an interface
and the others become classes where each class implements the interface.
Equip each class with a purpose statement; later we may write a paragraph
on the union, too.
Finally, we need to make up examples. While we cannot instantiate
the interface directly, we use it to provide a type for all the examples. The
latter are created from each of the implementing classes. For those who
may take over the program from us in the future, we should also explain in
a comment what the objects mean in the real world.
37
Section 5
38
check
From Scheme, we know that an MTLog doesnt contain any other information, so it doesnt contain any fields. A ConsLog, though, consists of two
values: an entry and another list of log entries; therefore the ConsLog class
requires two field definitions: one for the first Entry and one for the rest.
can we make the backarrow look different for this one picture?
ILog
MTLog
ILog
ConsLog
MTLog
Entry fst
ILog rst
Entry
Date d
...
ConsLog
Entry fst
ILog rst
Entry
Date
int day
int month
int year
Date d
...
Date
int day
int month
int year
39
the first field in ConsLog and Entry. The other field is rst; its type is ILog.
This left class diagram misses the arrow from rst to its class in this diagram, which the informal Scheme definition above contains. Drawing it,
we obtain the diagram in the right column in figure 18. The result is a diagram with a loop or cycle of arrows. Specifically, the diagram defines ILog
as the union of two variant classes with a reference from one of the two
classes back to ILog. We have created a pictorial data definition that is just
like the corresponding data definitions for lists that we know from How to
Design Programs.
Even if it werent a part of the design recipe for classes, we know from
our prior experience with How to Design Programs that we need examples
for self-referential data definitions. Otherwise, we never know whether
they make sense. Before we can make up examples of logs, however, we
need to translate the data definition (diagram) into a class hierarchy. Fortunately, it suffices to apply what we already know, because arrows merely
emphasize the type of a field, which is always a part of the class definition
anyway. See figure 19 for the result.
// a runners log
interface ILog {}
// the empty log
class MTLog implements ILog {
MTLog() {}
}
// an individual entry
class Entry { . . . }
Section 5
40
on June 5, 2003
on June 6, 2003
on June 23, 2003
...
5.3 miles
2.8 miles
26.2 miles
...
27 minutes
24 minutes
150 minutes
...
feeling good
feeling tired
feeling exhausted
...
Restaurant
IListing
MTListing
String name
String kind
String pricing
Place place
ConsListing
IListing rst
Restaurant fst
Place
int ave
int street
41
With the creation of these examples, we have just completed the standard design recipe for classes. We have also verified that a circular diagram
describes a perfectly fine way of representing information as data. Naturally its never clear whether such a definition does what we expected it to
do, so you should always experiment with several examples.
Exercises
Exercise 5.1 Translate these two objects of type ILog
ILog l5 = new ConsLog(e3,l1);
ILog l6 = new ConsLog(e3,l2);
into the runners world of logs. Assume these examples were constructed
in the context of the four examples above.
Exercise 5.2 Represent the following runners log as objects:
1. on June 15, 2004: 15.3 miles in 87 minutes, feeling great;
2. on June 16, 2004: 12.8 miles in 84 minutes, feeling good;
3. on June 23, 2004: 26.2 miles in 250 minutes, feeling dead;
4. on June 28, 2004: 26.2 miles in 150 minutes, good recovery.
For a second example in the same vein, lets resume our discussion of
the program that assists a visitor with restaurant selection in Manhattan:
. . . Develop a program that helps visitors navigate Manhattans
restaurant scene. The program must provide four pieces of information per restaurant: its name, the kind of food it serves, its
price range, and the closest intersection (street/avenue). . . .
Clearly, this program should deal with lists of restaurants, because a visitor
may, for example, wish to learn about all German restaurants in a certain
area or all Thai restaurants in a certain price range.
If we were designing a representation in Scheme, we would again use a
cons-based representation of restaurant listings:
;; A List of Restaurants (ILoR) is is one of:
;; a empty
;; a (cons Restaurant ILoR)
Section 5
42
// a list of restaurants
interface ILoR { }
// the empty list
class MTListing implements ILoR {
MTListing(){ };
}
// an individual restaurant
class Restaurant { . . . }
43
Next the listings l1, l2, and l3 contain French, German, and Chinese restaurants, respectively; the last listing contains all restaurants:
ILoR mt = new MTListing();
ILoR l1 = new ConsListing(ex1,mt);
ILoR l2 = new ConsListing(ex2,mt);
ILoR l3 = new ConsListing(ex3,mt);
ILoR all =
new ConsListing(ex1,new ConsListing(ex2,new ConsListing(ex3,mt)));
Exercises
Exercise 5.3 Consider a revision of the problem in exercise 3.1:
. . . Develop a program that assists real estate agents. The program deals with listings of available houses. . . . . . .
Make examples of listings. Develop a data definition for listings of houses.
Implement the definition with classes. Translate the examples into objects.
WeatherRecord
IWR
MTWR
Date d
TemperatureRange today
TemperatureRange normal
TemperatureRange record
ConsWR
IWR rst
WeatherRecord fst
Date
TemperatureRange
int day
int month
int year
int high
int low
Section 5
44
@
6
20
(40,30)
@15
@
R
@
&%
We could now also superimpose this compounded shape on another shape and so on. . . .
The new element in this problem statement is the goal of combining
two shapes into one. This suggests a new class that refines IShape from
figure 12. The purpose of the new class is to represent the combination of
two shapes. We call it SuperImp for that reason.
Figure 23 contains the class diagram for our revised problem. Like the
diagrams for lists, this diagram contains a cycle, specifying a self-referential
data definition; but unlike the list diagrams, this one has two arrows that go
from an implemented class back to the interface. As before, this difference
doesnt pose any problems for the translation into class definitions: see
figure 24.
45
IShape
Dot
Square
Circle
Combination
CartPt loc
CartPt loc
int size
CartPt loc
int radius
IShape bot
IShape top
// geometric shapes
interface IShape {}
class Dot
implements
IShape {
CartPt loc;
...
}
class Square
implements
IShape {
CartPt loc;
int size;
...
}
class Circle
implements
IShape {
CartPt loc;
int radius;
...
}
class SuperImp
implements
IShape {
IShape bot;
IShape top;
}
// Cartesian points on a computer monitor
class CartPt {
int x;
int y;
...
}
SuperImp(
IShape bot,
IShape top) {
this.bot = bot;
this.top = top;
}
Section 5
46
Here is the object that represents the shape from the problem statement:
new SuperImp(new Square(new CartPt(20,40),20),
new Circle(new CartPt(40,30),15))
The SuperImp object combines a square and a circle, placing the circle on
top of the square. Now look at these definitions:
CartPt cp1 = new CartPt(100, 200);
CartPt cp2 = new CartPt(20, 50);
CartPt cp3 = new CartPt(0, 0);
IShape s1 = new Square(cp1, 40);
IShape s2 = new Square(cp2, 30);
IShape c1 = new Circle(cp3, 20);
IShape sh1 = new SuperImp(c1, s1);
IShape sh1 = new SuperImp(s2, new Square(cp1, 300));
IShape sh3 = new SuperImp(s1, sh2);
To understand the purpose of these classes and the meaning of these specific objects, interpret sh1, sh2, and sh3 as figures on a grid.
The need for self-referential data definitions, like those of reading lists
and restaurant listings, also comes about naturally for data such as family
trees and river systems:
. . . The environmental protection
agency monitors the water qualt
ity of river systems. A river syss
/
\
tributary [2]
tem consists of a river, its tribumain [3] /
\ /
taries, the tributaries of the tribu\/
b
u
taries, and so on. The place where
\
/
main [3]
tributary [1]
a tributary flows into a river is
\
/
\ /
called a confluence. The rivers
a
|
endthe segment that ends in
main [4]
|
the sea or perhaps another river
m
is called its mouth; the initial
river segment is its source. . . .
Even a cursory look confirms that this is by far the most complex form
of information that we have encountered so far. When we are confronted
with something like that, it is best to make up at least one small example
and to study it in depth, which is why the problem comes with an artificial
map of a made-up river system. The example has three sourcess, t, and
uand two confluencesat b and a. The mouth of the entire system is at
47
m. The map depicts the system as if it had one main river, with two direct
tributaries. Each segment is labeled with a number, which represents its
name.
Mouth
IRiver
Location loc
IRiver river
Source
Confluence
Location loc
IRiver left
IRiver right
Location loc
Location
int x
int y
String name
Section 5
48
// a location on a river
class Location{
int x;
int y;
String name;
// a river system
interface IRiver{ }
// the source of a river
class Source implements IRiver {
Location loc;
Source(Location loc){
this.loc = loc;
}
Confluence(Location loc,
IRiver left,
IRiver right){
this.loc = loc;
this.left = left;
this.right = right;
}
49
class RiverSystemExample {
Location lm = new Location(7, 5, "m");
Location la = new Location(5, 5, "a");
Location lb = new Location(3, 3, "b");
Location ls = new Location(1, 1, "s");
Location lt = new Location(1, 5, "t");
Location lu = new Location(3, 7, "u");
IRiver s = new Source(ls);
IRiver t = new Source(lt);
IRiver u = new Source(lu);
IRiver b = new Confluence(lb,s,t);
IRiver a = new Confluence(la,cb,u);
Mouth mth = new Mouth(lm,ca);
RiverSystemExample() { }
Section 6
50
Exercise 5.7 Research the tributaries of your favorite river. Create a data
representation of the river and its tributaries. Draw the river system as a
schematic diagram.
Exercise 5.8 Modify the classes that represent river segments, mouths, and
sources so that you can add the names of these pieces to your data representation. Can you think of a river system that needs names for all three
segments involved in a confluence? Represent such a confluence with the
revised classes.
IPT
Coach
IPT team
MTTeam
PhoneTree
Player
IPT call1
IPT call2
Player p
String name
int phone
Exercise 5.9 Soccer leagues arrange its soccer teams into phone trees so
that they can quickly inform all parents about rain-outs. The league calls
the coach, who in turn calls the parents of the team captain. Each parent
then calls at most two other parents.
The class diagram in figure 28 contains the data definition for a program
that manages phone trees. Given these classes, one could create the data in
figure 29. Draw the phone tree there as a circle-and-arrow diagram. Each
circle corresponds to a player or coach. An arrow means that a player calls
some other player; it goes from the caller to the callee. Then develop the
class definitions that correspond to the given data definition.
In the preceding sections we have discussed more and more complex examples of class hierarchies. Designing a class hierarchy is the first and the
Player
cpt = new Player("Bob", 5432345);
Player
p1 = new Player("Jan", 5432356);
Player
p2 = new Player("Kerry", 5435421);
Player
p3 = new Player("Ryan", 5436571);
Player
p4 = new Player("Erin", 5437762);
Player
p5 = new Player("Pat", 5437789);
51
52
Section 6
53
Section 6
54
6.1 Exercises
Exercise 6.1 Design the data representation for a program that assists with
shipping packages for a commercial shipper. For each package the program needs to record the box size, its weight, information about the sender
and the recipient, and a URL for the customer so that the package can be
tracked. Hint: Use a String to represent a URL.
Exercise 6.2 Revise the data representation for a program that assists visitors in Manhattan (see page 21). Assume that a visitor is interested in
restaurants, museums, and shops. We have already studied what the program needs to represent about restaurants. Concerning museums, visitors
typically want to know the name of the museum, the price of admission,
and its hours. For shops, they also want to see its hours (assume the same
hours for every day), but also what kind of items they sell. Of course, visitors also need to find the restaurants, museums, and shops; that is, the data
representation needs to contain locations.
Exercise 6.3 Design the data representation for a program that manages
the schedule for one route of a train. A schedule records the departure
55
station and time; the destination station and estimated arrival time; and all
the stops in between. For now, identify a stop on the route with its name.
Exercise 6.4 Design the data representation for a program that assists a
real-estate agent. A real estate agent sells several different kinds of properties: single family houses (see problem 3.1), condominiums, and town
houses. A typical customer needs to know the address of the property, the
living area (in square feet), and the asking price (in dollars). For a single
family house, the customer also wants to know the land area and number
of rooms. For a condominium, the customer wants to know the number
of rooms and whether it is accessible without climbing stairs. For a town
house, the client is often interested in how much garden area town houses
have.
Exercise 6.5 Design the data representation for a program that creates graphical user interfaces (GUIs). The basic component of a GUI is one of the
following:
BooleanView
TextFieldView
OptionsView
ColorView
Each of these kinds of components contains a label and some additional
data, which for now doesnt play a role.
To arrange basic GUI components in a grid, GUI software systems provide some form of table. A table consists of several rows; each row is a
series of GUI components. Naturally, tables can be nested to allow for complex layouts; that is, they must also be GUI components (though without
label).
Exercise 6.6 Design the data representation for a program that tracks library checkouts. The library has books, CDs, and DVDs. Each item has a
catalog number and a title. For each book the librarian also needs to record
the name of the author, and the year the book was published. For CDs, the
librarian also records the artist, and the number of tracks. For DVDs, the
record indicates the kind of DVD (comedy, drama) and the length of play.
Section 6
56
manager imagines a game freely named after H. G. Wellss science fiction novel.
57
Section 6
58
59
UFOWorld
UFO
Color colorUFO
Posn location
int WIDTH
int HEIGHT
Color BACKG
UFO ufo
AUP aup
IShots shots
IShots
MTShots
AUP
Color aupColor
int location
Shot
ConsShots
Shot first
IShots rest
Color shotClr
Posn location
Section 6
60
IColor
Blue
Green
Red
White
Yellow
Black
Posn
int x
int y
at the top. The latter defines an interface dubbed IColor with classes implementing Red, Green, Yellow, and Blue (among others). To get access to these
classes, use
import colors.;
Figure 31 provides a diagram of the two libraries.
The second step is to draw a diagram that reflects what we know about
the various kinds of information. Figure 30 contains the result of this effort.
There are seven boxes: one for an interface (IShots) and six for classes. Three
of the latter directly correspond to physical objects: UFO, AUP, and Shot;
the other four are: IShots, MtShots, ConsShots, and UFOWorld. Roughly
speaking, these four classes exist so that we can keep track of compounds
of information. For example, the UFOWorld class contains all the pieces
that play a role in the game. Similarly, IShots is an interface that represents
all classes of list of shots. Note how the diagram treats classes from libraries
just like int and String.
Third, you must translate the class diagram into classes and interfaces.
Most of this translation is a mechanical step but remember that it also demands a decision as to which properties of objects are constant and which
ones are unique and initialized during the construction of the object. To
see how this decision making works, take a look at the UFOWorld class in
figure 32. The values of the first three fields are objects whose appearance
varies over time. The nature of the remaining three fields in UFOWorld is
radically different, however. They describe aspects of the world that al-
61
AUP(int location) {
this.location = location;
}
UFO(Posn location) {
this.location = location;
}
ways remain the same; they are constants and we know their values. We
therefore add initializations to the fields and omit corresponding parameters and equations from the constructor.
Translating the diagrams for AUP and UFO into actual classes shows
that all of them have a constant color field and all other fields are objectspecific: see the bottom of figure 32.
Finally, given your experience, the creation of interfaces and classes for
Shot and list of Shots are routine. They follow the pattern that we have seen
several times now, without any deviation. See figure 33 for details.
Section 6
62
Shot(Posn location) {
this.location = location;
}
Our last and very final step is to create examples for each class. As suggested by the recipe, this process proceeds bottom up, meaning we first
create instances of those classes that dont contain any instance of the other
(relevant) classes. Here we start with AUP and end with UFOWorld, which
contains all kinds of objects: see figure 34. The figure shows how in a
project of any size examples are collected in a separate class. Also, each
example should come with a short explanation of what it represents. As we
gain programming experience, we may omit such explanations from simple examples and expand those for complex examples. Still, having such
explanations around strongly increases the likelihood that we can read, understand, and use these examples later when it is time to create examples
of a programs inputs and outputs.
Exercises
Exercise 6.10 Collect the class definitions in this section and evaluate them
in ProfessorJ. Inspect the default instance of WoWExamples.
63
class WoWExamples {
// an anti-UFO platform placed in the center:
AUP a = new AUP(100);
// a UFO placed in the center, near the top of the world
UFO u = new UFO(new Posn(100,5));
// a UFO placed in the center, somewhat below u
UFO u2 = new UFO(new Posn(100,8));
// a Shot, right after being fired from a
Shot s = new Shot(new Posn(110,490));
// another Shot, above s
Shot s2 = new Shot(new Posn(110,485));
// an empty list of shots
IShots le = new MtShots();
// a list of one shot
IShots ls = new ConsShots(s,new MtShots());
// a list of two shots, one above the other
IShots ls2 = new ConsShots(s2,new ConsShots(s,new MtShots()));
// a complete world, with an empty list of shots
UFOWorld w = new UFOWorld(u,a,le);
// a complete world, with two shots
UFOWorld w2 = new UFOWorld(u,a,ls2);
WoWExamples() { }
Intermezzo 1
64
65
Each field declaration must come with a type and a name, followed
by an optional initialization. The fieldName always starts with a lowercase letter; all other words in the name start with an uppercase,
yielding a camel-back shape for such names.
Constraint: Each field declaration introduces a name that is unique
inside the class.
5. The last element of a class is a constructor definition:
class ClassName {
Type fieldName [= Expr];
...
ClassName(Type fieldName, . . . ) {
this.fieldName = fieldName;
...
}
}
All fields without initialization are listed in the parameter part; for
each parameter, the constructor body contains one equation.
6. A Type is one of:
(a) int, double, boolean, char
(b) Object, String,
(c) ClassName,
(d) InterfaceName.
7. An Expr is one of:
(a) a constant such as 5 or true;
(b) a this.fieldName; or
(c) a constructor call.
8. Constructor calls are expressions that create instances of a class:
new ClassName(Expr,. . . )
The sequence of expressions is as long as the sequence of constructor
parameters. The first expression is for the first field parameter, the
second expression is for the second field parameter, and so on.
Intermezzo 1
66
ImportSpec
colors
InterfaceDefinition
draw
ClassDefinition
geometry
an I MPORT S PEC is
import LibN.;
an I NTERFACE D EFINITION is
interface InterfaceN {}
Object
a C LASS D EFINTION is:
String
class ClassN [ implements InterfaceN ] {
Type FieldN [ = Expr ];
ClassN
...
InterfaceN
ClassN(Type FieldN, . . . ) {
this.FieldN = FieldN;
an E XPR is one of:
...
constant (e.g., 5, true)
}
}
this.FieldN
ConstructorCall
a C ONSTRUCTOR C ALL is
new ClassN(Expr, . . . )
67
Meaning
You can specify only one form of computation in the Beginner language of
ProfessorJ: the creation of objects. Specifically, a constructor call creates an
instance of a class. Such an instance is a single piece of data that possibly
consists of many different pieces of data. To be precise, an instance consists
of as many pieces of data as there are fields in the class. Each field contains a value, which is either computed directly next to the field or in the
constructor.
Consider the following program, which consists of one class definition:
class Posn {
int x;
int y;
Posn(int x, int y) {
this.x = x;
this.y = y;
}
In this context, you can evaluate a constructor call such as this one:
new Posn(3,4)
This creates an instance of Posn whose x and y fields contain the values 3
and 4, respectively. If you enter the expression at the prompt of ProfessorJs
interactions window, you see this response:
Posn(x = 3, y = 4)
In other words, the interactions window shows you the pieces of data that
make up an object.
Contrast the first example with this second program:
Intermezzo 1
68
class Ball {
int radius = 3; // pixels
int x;
int y;
Ball(int x, int y) {
this.x = x;
this.y = y;
}
}
Creating an instance of Ball uses just two ints: one for the x field and one for
y. The resulting instance, however, has three fields, that is, the interactions
window displays a constructor call such as
new Ball(1,2)
as an object with three fields:
Ball(radius = 3,
x = 1,
y = 2)
Next, if your program contains the above definition of Posn and the
following UFO class definition
class UFO {
Posn location;
int WIDTH = 20
int HEIGHT = 2;
int RADIUS = 6;
UFO(Posn location) {
this.location = location;
}
69
WIDTH = 20,
HEIGHT = 2,
RADIUS = 6)
Intermezzo 1
70
This sequence of definitions consists of two class definitions, both using the
name Ball, which violates a basic constraint about programs. The following
single class definition violates a similar constraint for fields, using x twice:
class Ball {
int x;
int x;
Ball(int x, int y) {
this.x = x;
this.y = y;
}
}
Exercises
Exercise 7.1 Identify the grammatical correct programs and class definitions from the following list; for incorrect ones, explain the error message
that ProfessorJ produces:
1. a program that consists of an interface and a class:
interface Automobile { }
class Automobile {
int consumption; // miles per gallon
Automobile( int consumption) {
this.consumption = consumption;
}
}
2. another program that consists of an interface and a class:
interface IVehicle { }
class automobile implements IVehicle {
int Consumption; // miles per gallon
automobile(int Consumption) {
this.Consumption = Consumption;
}
}
71
Intermezzo 1
72
After clicking RUN, evaluate the following three expressions in the interactions window:
1. new Ball(3,4)
2. new Ball(new Posn(3,4))
3. new Ball(3)
Predict what happens. For incorrect ones, explain the error message.
Exercise 7.3 Can you spot any grammatical mistakes in these definitions:
1.
class Ball {
int x;
Ball(int x, int x) {
this.x = x;
}
}
2.
class Ball {
int x;
int y;
int x;
Ball(int x, int y) {
this.x = x;
this.y = y;
}
}
3.
class Ball {
int x;
int y;
Ball(int x, int y) {
this.x = x;
this.y = y;
this.x = x;
}
}
73
For incorrect ones, explain the error message that ProfessorJ produces.
Since type errors arent covered in How to Design Programs, lets study
a couple of examples before we discuss their general nature. Suppose you
define a class like this:
class Weight {
int p = false;
Weight() {}
}
Although this definition is grammatically correct, its field declaration is
wrong anyway. Specifically, while the field declaration specifies that p is an
int, the initialization equation has false on the right-hand side, which is a
boolean value not an int.
Similarly, if you enter
new Weight(false)
at the prompt of the interactions window in the context of this definition:
class Weight {
int p; // pounds
Weight(int p) {
this.p = p;
}
}
ProfessorJ signals an error with the message
Constructor for Weight expects arguments with
type int, but given a boolean ...
and highlights the constructor call. The reason is that the constructor definition specifies that the constructor consumes an int but the constructor call
supplies a boolean instead.
In general, type errors are mismatches between a specified (or expected)
type for an expression and the type that the expression actually has. For the
first example, the difference between expected type and actual type was immediately apparent. For the second example, the specified type is attached
to a parameter of the constructor; the actual type of false is boolean.
To generalize properly, we need to understand what it means to determine an expressions ACTUAL TYPE. Every primitive value has an obvious
Intermezzo 1
74
actual type. For example, 5s actual type is int; falses type is boolean; 4.27
is a double; and "four" belongs to String. A constructor call of the shape
new ClassName(Expr, . . . )
has the actual type ClassName. Finally, the last kind of expression in our
grammar is the name of a field. When a field name occurs as an expression,
its actual type is the type that comes with the field declaration.
Sadly, determining the actual type of an expression isnt quite enough;
we must also discuss SUBTYPing. If the class ClassName implements the
interface InterfaceName, then the latter is a SUBTYPE of the former. Alternatively, people say ClassName is below InterfaceName. When Java matches
types, it often allows an expression with a subtype of the expected type.
Now that we understand what an actual type is and what subtyping
is, understanding a type mismatch is relatively straightforward. Here are
some canonical examples of the mismatches that can happen in ProfessorJs
Beginner language:
1. if a field declaration comes with an initialization equation then the
field type may not match the actual type of the expression on the
right-hand side of the = sign.
Example:
class Ball {
int radius = 4.2; // int doesnt match double
...
The problem shows up in other guises, too. Consider the following
(partial) union:
Example:
interface IVehicle { }
class Boat implements IVehicle {
Boat() { }
}
If the program also contains this definition,
class SUV {
SUV() { }
}
75
then the following example class contains one correct and one incorrect field declaration:
class ExamplesOfVehicles {
IVehicle one = new Boat();
IVehicle two = new SUV(); // SUV is unrelated to IVehicle
ExamplesOfVehicles() { }
}
Specifically, the second field declaration in ExamplesOfVehicles specifies on the left-hand side that two should always stand for objects
of type IVehicle. The right-hand side, however, creates an instance of
SUV, which doesnt implement IVehicle and is therefore unrelated to
the interface as a type.
2. if a field declaration (without initialization) specifies one type and
the corresponding parameter of the constructor has a different type,
then the field type doesnt match the type of the expression in the
corresponding constructor equation:
Example:
class Ball {
int radius;
Ball(double radius) {
this.radius = radius;
}
...
}
3. if a constructor declaration specifies the type some parameter as one
type, then the corresponding argument in a constructor call for this
class must have an actual type that is below the expected type:
Example:
class Weight {
int p;
Weight(int p) {
this.p = p;
}
}
Intermezzo 1
76
77
out about a new construct of a language, it may also introduce new kinds
of type errors.
Exercises
Exercise 7.4 Identify what kind of type errors the following programs contain:
1.
class Employee {
String fst;
String lst;
Employee(String fst, int lst) {
this.fst = fst;
this.lst = lst;
}
}
2.
class Employee {
String fst;
String lst;
Employee(String fst, String lst) {
this.fst = fst;
this.lst = lst;
}
}
new Employee("Matthias", 1)
3.
class Customer {
String name;
Customer(String name) {
this.name = name;
}
}
Intermezzo 1
78
new Employee("Matthias", 1)
4.
interface IPerson {}
class Employee implements IPerson {
String fst;
String lst;
Employee(String fst, int lst) {
this.fst = fst;
this.lst = lst;
}
}
class Customer {
String name;
Customer(String name) {
this.name = name;
}
}
class Transaction {
IPerson c = new Customer("Kathy Gray");
IPerson e = new Employee("Matthew", "Flatt");
Transaction() { }
}
Explain them in terms of the enumeration of type errors in this section.
Exercise 7.5 Consider the following program:
interface IRoomInMUD { }
class TowerRoom implements IRoomInMUD { . . . }
class WidowsWalk { . . . }
79
class SetUp {
IRoomInMUD room;
SetUp(IRoomInMUD room) {
this.room = room;
}
}
If you hit RUN and evaluate the following two constructor calls, which one
creates an object and which one signals a type error:
1. new SetUp(new WidowsWalk(. . . ))
2. new SetUp(new TowerRoom(. . . ))
What kind of type error is signaled?
PICTURE:
81
82
Intermezzo 1
TODO
make sure to introduce import draw.* in chapter 2 properly
does the code need import hints?
introduce and explain String.valueOf early
structural (visual) clues for the examples of section 12 (shapes)?
somewhere in the abstraction chapter we need to place a warning that
lifting textually identical methods isnt always possible.
introduce error properly
should we encourage students to develop stubs instead of just headers?
Functional Methods
II
Once you have designed a data representation for the information in your
problem domain, you can turn your attention to the design of functions
on this data. In an object-oriented language, functions are implemented
as methods. In this chapter, you will learn to design methods following
the same systematic discipline that you already know from How to Design
Programs. It starts with a brief introduction to expressions, in general, and
method calls, in particular, and then explains how to add methods to more
and more complex forms of class hierarchies. The organization of this chapter is parallel to that of chapter I.
ProfessorJ:
Interactions Window
Section 8
84
symbol
!
&&
||
arity
unary
binary
binary
parameter types
boolean
boolean, boolean
boolean, boolean
result
boolean
boolean
boolean
example
!(x < 0)
a && b
a || b
binary
binary
binary
binary
numeric, numeric
numeric, numeric
numeric, numeric
numeric, numeric
numeric
numeric
numeric
numeric
x+2
x2
x2
x/2
<
<=
>
>=
==
binary
binary
binary
binary
binary
numeric, numeric
numeric, numeric
numeric, numeric
numeric, numeric
numeric, numeric
boolean
boolean
boolean
boolean
boolean
x<2
x <= 2
x>2
x >= 2
x == 2
logical negation
logical and
logical or
addition
subtraction
multiplication
division
less than
less or equal
greater than
greater or equal
equal
example: . . . (0 < x) && (x < 10) . . . determines whether 0 is less than x (int
or double) and x is less than 10.
Like mathematics (and unlike Scheme), Java comes with precedence
rules so that 0 < x && x < 10 also works as expected. If you recall all these
precedence rules, and if you have the courage to guess at precedences when
you see new and unfamiliar operators, drop the parentheses; it youre like
us, you will use parentheses anyway, because you never know whos going
to read your program.
Figure 36 introduces some basic arithmetic, relational, and boolean operators for int, double, and boolean. Take a quick look now, mark the page,
and consult the table when a need for these operators shows up in the following sections and exercises; its also okay to guess on such occasions and
to check your guess in ProfessorJs interactions window.
For Java, the primitive type String is a class just like those we have defined in the first chapter. Each specific string is an instance of this class. The
String class is a bit unusual in that Java doesnt require us to write
new String("hello world")
85
new String("hello") is an expression that produces a string, its result is not the
same as "hello" but we ignore the difference for now.
ProfessorJ:
Interactions Window
Section 9
86
is a method invocation of concat for the String object "hello" with a second
argument: "world". The purpose is to compute the string that results from
concatenating "world" to the end of the primary argument, just like (stringappend "hello" "world") would. Of course, the result is "helloworld".
In general, a method call has this shape:
eObject.methodName(expression, . . . )
Here eObject is any expression that evaluates to an object and methodName
is a method defined in the class of which the object is an instance. As this
chapter shows, this could be the name of a field, the parameter of a method,
or an expression that computes an object:
"hello".concat(" ") .length()
In this example, the gray-shaded part is an expression that evaluates to a
String object, via a call to concat. Once the expression is evaluated (to "hello
"), the length method is called and produces 6.
method
length
concat
trim
toLowerCase
toUpperCase
equals
endsWith
startsWith
additional parameters
result
int
example
"abc".length()
87
some basic String methods. Their names and purpose statements suggest
what they compute; explore their behavior in the Interactions window.
Section 10
88
Next we must make up some examples, that is, function calls for cost
that illustrate what it should produce when given certain arguments. Naturally, 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. Since cost consumes a structurean instance of coffeeyou 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 the coffee structure has three fields. Each expression extracts one value from a-coffee, the
parameter.
The transition from the template to the full function definitionthe fifth
stepstarts 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 dont 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
methods primary argument. Thus, if the Coffee class already had a cost
method, we could write in the example section
new Coffee("Kona", 2095, 100).cost()
and expect this method call to produce 209500.
Lets try to develop this method systematically, following our wellknown design recipe. First we add a contract, a purpose statement, and
a header for cost to the Coffee class:
89
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 of cost 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 doesnt consume anything, but remember that cost is 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 purpose statementreminding readers of the role of the special argument
and method body to refer to the instance of Coffee on which cost is invoked:
inside of Coffee :
// to compute the total cost of this coffee purchase [in cents]
int cost() { . . . 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, lets reformulate the functional examples in Java:
Coffee c = new Coffee("Kona", 2095, 100)
...
check c.cost() expect 209500
That is, in the context of an existing example, we invoke the cost method.
Reformulate the other examples from the Scheme approach in Java.
The next step is to formulate the template. Recall that the template expresses what we know about the argument(s). In our running example, the
ProfessorJ:
Examples & Tests
Section 10
90
input is this instance of the class. Each instance consists of three pieces
of data: the kind, the price, and the weight. In Scheme, we use special functions, the selectors, to extract the relevant pieces. In Java, we access an
objects fields with the dot notation. In general, we write:
object . field
Since we wish to access the fields of this object, we write this.kind, this.
price, and this.weight in the method body to create the template:
inside of Coffee :
// to compute the total cost of this coffee purchase
int cost() {
. . . this.kind . . . this.price . . . this.weight . . .
}
The rest is easy. We must decide which pieces of the template are relevant and how to use them. In our running example, the two relevant pieces
are this.price and this.weight. If we multiply them, we get the result that we
want:
inside of Coffee :
// to compute the total cost of this coffee purchase
int cost() {
return this.price this.weight;
}
ProfessorJ:
Automatic Tests
The return keyword 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 diagram, the box for Coffee consists 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
methods name doesnt 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.
Finally, the figure extends the examples class from chapter I (page 15).
In addition to the sample instances of Coffee, it also contains three test fields:
testKona, testEthi, and testColo. 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
91
Coffee
String kind
int price [in cents per pound]
int weight [in pounds]
int cost()
CoffeeExamples() { }
with an expected value and produces true or false. When you place the two
classes into the definitions window and run the program, ProfessorJ creates
an instance of CoffeeExamples and determines how many of the fields with
test in their name are true.
Some methods must consume more data than just this. 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 the price of the sales record.
First we write down the purpose statement and the signature:
Section 10
92
inside of Coffee :
// to determine whether this coffees price is more than amt
boolean moreCents(int amt) { . . . }
The purpose statement again reminds us that moreCents consumes two arguments: this and amt. Second, we make a couple of examples:
check new Coffee("Kona", 2095, 100).moreCents(1200) expect true
check new Coffee("Columbian", 950, 200).moreCents(1000) expect false
To practice your design skills, explain the expected results.
The template for this method is exactly that for cost:
inside of Coffee :
// to determine whether this coffees price is more than amt
boolean moreCents(int amt) {
. . . this.kind . . . this.price . . . this.weight
}
We do not have to include anything about the second argument, amt, because it is a part of the signature and its type is just int, i.e., atomic data.
The only relevant pieces of data in the template are amt and this.price:
inside of Coffee :
// to determine whether this coffees price is more than amt
boolean moreCents(int amt) {
return this.price > amt;
}
Dont 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 additional test fields in CoffeeExamples (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 consumes two instances of Coffee. We call them this and, by convention, that in
the purpose statement:
inside of Coffee :
// to determine whether this coffee sale is lighter than that coffee sale
boolean lighterThan(Coffee that) { . . . }
93
Section 10
94
Coffee
String kind
int price [in cents per pound]
int weight [in pounds]
int cost()
boolean moreCents(int amt)
boolean lighterThan(Coffee that)
95
Section 10
96
)[
)[
0
5000
10000
This picture is a good foundation for the construction of both the example
step as well as the template step later.
First, however, we must represent the major problem information
bank certificates of depositin our chosen language, i.e., as a class:
// represent a certificate of deposit
class CD {
String owner;
int amount; // cents
97
Section 10
98
if (condition) {
return expression1; }
else {
if (condition2) {
return expression2; }
else {
return expression3; } }
Here we replaced statement2 with the gray-shaded if statement. The complete statement thus distinguishes three situation:
1. if condition holds, the computation proceeds with return expression1;
2. if condition doesnt hold but condition2 holds; then the computation
proceeds with return expression2;
3. and if neither condition1 nor condition2 evaluates to true, then the computation proceeds with return expression3.
The analysis and the examples distinguish three situations so we do
need two if statements as shown above:
inside of CD :
// compute the interest rate for this account
double interest() {
if (0 <= this.amount && this.amount < 500000) {
. . . this.owner . . . this.amount . . . }
else { if (500000 <= this.amount && this.amount < 1000000) {
. . . this.owner . . . this.amount . . . }
else {
. . . this.owner . . . this.amount . . . }
}
}
From the first design recipe we know to use this.amount; the tests come
from the pictorial analysis of the problem.
The ownership data naturally plays no role for the computation of the
interest rate. So finishing the definition from the template is easy:
99
inside of CD :
// compute the interest rate for this account
double interest() {
if (0 <= this.amount && this.amount < 500000) {
return 2.00 this.amount; }
else { if (500000 <= this.amount && this.amount < 1000000) {
return 2.25 this.amount; }
else {
return 2.50 this.amount; }
}
}
Your task now is to formulate an examples class and the method examples
as tests in the same class. When you are done you may also ponder the
following challenge:
. . . The bank has decided that keeping track of fractional cents
no longer makes any sense. They would like for interest to return an int. . . .
Find the documentation for Javas Math class and read up on Math.round.
Then modify the design of interest appropriately, including the tests.
Lets look at a second example. Suppose your manager asks you for
some exploratory programming:
// represent a falling
// star on a 100 x 100 canvas
class Star {
int x = 20;
int y;
int DELTA = 5;
Star(int y) {
this.y = y;
}
For good measure, your manager has already designed a data representation according to the design recipe. A falling star has a location, which
means x and y coordinates, and it moves downward at some fixed rate.
Hence the class has three fields. The x coordinate and the rate of descend
ProfessorJ:
Testing with doubles
Section 10
100
DELTA are always the same for now; the y coordinate increases14 continuously.
Your task is to develop the method drop, which creates a new star at a
different position. Following the design recipe you extract a concise purpose statement for the method from the problem statement:
inside of Star :
// drop this Star by DELTA pixels,
// unless it is on (or close) to the ground
Star drop() {
. . . this.y . . . this.DELTA . . .
}
Here the purpose statement just reformulates two sentences from the problem statement. It naturally suggests that there are two distinct kinds of
stars: one that is falling and one that has landed. This, in turn, means that
we need at least two kinds of examples:
Star s = new Star(10)
Star t = new Star(100)
that on a computer, the origin of the Cartesian grid is in the upper left.
Going to the right increases the x value, going down increases the y value.
101
Star
String kind
int x = 20
int y
int DELTA = 5
int drop()
inside of Star :
// drop this Star by DELTA pixels, unless it is on or near the ground
Star drop() {
if (this.y + this.DELTA >= 100)
...
else // the star is in the middle of the canvas
...
}
Remember that if you can distinguish different intervals in a numeric type
or different cases in another atomic type of data, a template distinguishes
as many situations as there are sub-intervals (cases).
Now take a look at figure 40, which contains the full method definition
for drop. If the condition holds, the method returns a star at height 100 (the
ground level); otherwise, it actually creates a star that has dropped by a
few pixels.
A bit of reflection suggests that we could have easily decided to distinguish three different situations:
Section 10
102
inside of Star :
97
100
On the left you see a number line with the appropriate sub-intervals, just
like in How to Design Programs. There are three: from 0 to 97 (exclusive);
from 97 (inclusive) to 100 (exclusive); and 100. On the right, you see a
template that distinguishes the three situations. It uses two if statements,
one followed by another. In principle, you can chain together as many of
them as are necessary; the details are explained in the next intermezzo.
Exercises
Exercise 10.5 Modify the Coffee class from figure 38 so that cost takes into
account bulk discounts:
. . . Develop a program 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. If the sale is for less than 5,000 pounds, there is no discount. For sales of 5,000 pounds to 20,000 pounds, the seller
grants a discount of 10%. For sales of 20,000 pounds or more,
the discount is 25%. . . .
Dont forget to adapt the examples, too.
Exercise 10.6 Take a look at this following class:
103
Design the method sizeString for this class. It produces one of three strings,
depending on the number of pixels in the image:
1. "small" for images with 10,000 pixels or fewer;
2. "medium" for images with between 10,001 and 1,000,000 pixels;
3. "large" for images that are even larger than that.
Remember that the number of pixels in an image is determined by the area
of the image.
Exercise 10.7 Your physics professor would like to simulate an experiment
involving bouncing balls. Design a class that represents a ball that is falling
on a 10 x 100 canvas at a rate of DELTA. That is, each time the clock ticks,
the ball drops by DELTA pixels.
When the ball reaches the bottom of the canvas, it bounces, i.e., it reverses course and travels upwards again. The bounce is perfect, meaning
the ball travels the full distance during the bounce. Put different, if the ball
is far enough away from a wall, it just travels DELTA pixels. If it is too close
for that, it drops by whatever pixels are left and then reverses course for
the remaining number of pixels. As it reverses course, it continues to travel
at the same speed.
Design the method move, which simulates one step in the movement of
the ball.
As you design conditional methods, dont forget the design recipe from
How to Design Programsfor just this kind of function. If it is complex, draw a
number line to understand all the intervals (cases, enumerated items). Pick
examples from the interior of each interval and for all the borderline cases.
Section 10
104
// a certificate of deposit
class CD {
String owner;
int amount; // cents
CD(String owner, int amount) {
this.owner = owner;
this.amount = amount;
}
// compute the interest rate (in %) for this account
double rate() {
if (0 <= this.amount
&& this.amount < 500000) {
return 2.00; }
else { if (500000 <= this.amount
&& this.amount < 1000000) {
return 2.25; }
else {
return 2.50; }
}
}
CD
String owner
int amount
int rate()
int payInterest()
105
For a concrete example, consider figure 41. It displays a class for representing certificates of deposit that can also compute the amount of interest
that the bank must pay. The relevant method is called payInterest. It first
determines the appropriate interest rate with this.rate(), multiplies by the
deposit amount, and finally divides it by 100 because the rate is represented
as a percentage.
// information about an image
class Image {
int width;
int height;
String source;
Image(int width, int height, String source) {
this.width = width;
this.height = height;
this.source = source;
}
// is this image large?
String sizeString() {
if (this.area() <= 10000) {
return "small"; }
else { if (this.area() <= 1000000) {
return "medium"; }
else {
return "large"; }
}
}
Image
int width
int height
String source
String sizeString()
int area()
A second example appears in figure 42. The Image class contains two
methods: sizeString and area. The former refers to the latter, because its result depends on the area that the image represents. Specifically, both conditions in sizeString evaluate this.area() which computes the area of the image
Section 10
106
Exercises
Exercise 10.8 Study this class definition:
// the daily percipitation of three consecutive days
class Precipitation {
int day1;
int day2;
int day3;
Precipitation(int day1, int day2, int day3) {
this.day1 = day1;
this.day2 = day2;
this.day3 = day3;
}
Add the method average to this class definition. Follow the design recipe
and reuse existing methods, if possible.
Exercise 10.9 Design the class JetFuel, whose purpose it is to represent the
sale of some quantity of jet fuel. Each instance contains the quantity sold
(in integer gallons), the quality level (a string), and the current base price
of jet fuel (in integer cents per gallon). The class should come with two
methods: totalCost, which computes the cost of the sale, and discountPrice,
which computes the discounted price. The buyer gets a 10% discount if the
sale is for more than 100,000 gallons.
107
canvas
origin
aRectangle
Even a cursory glance at this picture suggests that the shortest distance
between the Rectangle and the origin is the distance between its top-left
corner (anchor point) and the origin (see arrow).
The obvious conclusion from this problem analysis is that the problem
is really asking for the development of two methods: one for Rectangle and
one for CartPt. The second measures the distance of a CartPt to the origin
and the first measures the distance of a Rectangle to the origin:
inside of CartPt :
inside of Rectangle :
// to compute the distance of
// to compute the distance of
// this Rectangle to the origin
// this CartPt to the origin
double distance0() { . . . }
double distance0() { . . . }
The two purpose statements and method signatures just restate our intentions with code. They also lead straight to the second step, the development
of examples for both methods:
Section 11
108
Rectangle
CartPt tlCorner
int width
int height
CartPt
int x
int y
class ShapeExamples {
CartPt p = new CartPt(3,4);
CartPt q = new CartPt(5,12);
Rectangle r = new Rectangle(p,5,17);
Rectangle s = new Rectangle(q,10,10);
Rectangle(CartPt tlCorner,
int width,
int height) {
this.tlCorner = tlCorner;
this.width = width;
this.height = height;
}
ShapeExamples() { }
CartPt(int x, int y) {
this.x = x;
this.y = y;
}
Make sure you understand the expected result for each example and that
you can associate it with the interpretation of the above sample picture.
Now its time to turn our attention to the template. According to the
basic design recipe, we first need to add one selector expression per field in
each method body:
109
inside of Rectangle :
inside of CartPt :
double distance0() {
double distance0() {
. . . this.tlCorner . . .
. . . this.x . . .
. . . this.width . . .
. . . this.y . . .
. . . this.height . . .
}
}
The method template in Rectangle contains three expressions because the
class definition contains three fields; the same reasoning applies to the template in CartPt. But, remember that the purpose of a template is to translate
the organization of the data definition into expressions. And given that we
use diagrams as data definitions, there is clearly something missing: the
arrow from Rectangle to CartPt.
A moments thought suggest that this containment arrow suggests the
natural connection between the two method templates:
inside of Rectangle :
inside of CartPt :
double distance0() {
double distance0() {
. . . this.tlCorner.distance0() . . .
. . . this.x . . .
. . . this.width . . .
. . . this.y . . .
. . . this.height . . .
}
}
The gray-shaded method call expresses this containment arrow. It reiterates what we discovered through a careful analysis of the picture above. In
general terms, the arrow says that CartPt is a separate class; the method call
says that if we want to deal with properties of the tlCorner, we delegate this
computational task to the corresponding methods in its class.
From here, completing the method definitions is simple. The distance0
method in Rectangle invokes the distance0 method on the tlCorner object.
This replaces the task of computing the distance of the Rectangle to the origin with the task of computing the distance of a single point to the origin.
Whatever the latter produces is the result of the former, toojust like our
geometric reasoning suggested and the template development confirmed.
Do you remember the formula for computing the distance between two
points? If not, you will have to find (and pay) a geometry expert who
knows it:
q
x2 + y2
Section 11
110
class Rectangle {
CartPt tlCorner;
int width;
int height;
Rectangle(CartPt tlCorner, . . . )
int width,
int height) { . . . }
Rectangle
CartPt tlCorner
int width
int height
double distance0()
CartPt
int x
int y
double distance0() }
class CartPt {
int x;
int y;
CartPt(int x, int y) { . . . }
Lets practice this design with the weather-record example from figure 11. A weather record consists of a date, three temperature ranges, and
the amount of todays precipitation. The diagram contains three classes:
one for WeatherRecords, one for Dates, and one for TemperatureRanges. If
your company has contracts with meteorologists, you may one day encounter the following problem on your desk:
111
WeatherRecord
Date d
TemperatureRange today
TemperatureRange normal
TemperatureRange record
double precipitation
Date
TemperatureRange
int day
int month
int year
int high
int low
For good measure, the problem statement includes the old class diagram
for the problem. As you can tell, it contains not just one containment arrow,
like the previous example, but four of them.
Given the extra complication, lets first make examples of these objects:
see figure 45, left-hand side. Interpret the examples in real world; try to
think of places that might have such weather records.
Now that you have a diagram and examplesit is easy to imagine the
actual class definitions by nowyou can start with the design recipe for
methods. Figure 45 (right-hand side) contains a class diagram with proper
method signatures and a purpose statement for the main method. The
problem statement dictates the name and purpose statement for WeatherRecord; for the other two classes, the diagram contains only basic signatures, because we dont know yet what we need from them.
Using the class diagram, you can develop the method templates in a
straightforward manner:
// WeatherRecord
// TemperatureRange
// Date
int differential() {
??? lll() {
??? nnn() {
. . . this.date.lll() . . .
. . . this.day . . .
. . . this.low . . .
. . . this.today.nnn() . . .
.
. . this.month . . .
. . . this.high . . .
. . . this.normal.nnn() . . .
. . . this.year . . .
}
. . . this.record.nnn() . . .
}
. . . this.precipitation . . .
}
The template in WeatherRecord contains five expressions because there are
five fields in the class definition. Due to the types of the first four fields, the
first four selectors are equipped with method calls to other method templates along the containment arrows. The other two method templates are
Section 11
112
WeatherRecord
Date d
TemperatureRange today
TemperatureRange normal
TemperatureRange record
double precipitation
class RangeExamples {
Date d1 =
new Date(2,9,1959);
Date d2 =
new Date(8,8,2004);
Date d3 =
new Date(12,12,1999);
TemperatureRange tr1 =
new TemperatureRange(66,88);
TemperatureRange tr2 =
new TemperatureRange(70,99);
TemperatureRange tr3 =
new TemperatureRange(28,31);
Date
TemperatureRange
int day
int month
int year
int high
int low
??? nnn()
??? lll()
WeatherRecord r1 =
new(d1,tr1,tr2,tr3,0);
WeatherRecord r2 =
new(d2,tr2,tr3,tr1,10);
WeatherRecord r3 =
new(d3,tr3,tr1,tr2,9);
entirely routine.
A look at the template in WeatherRecord suffices to complete the definition:
inside of TemperatureRange :
inside of WeatherRecord :
int differential() {
int difference() {
return this.today.difference();
return this.high this.low ;
}
}
The differential method requires only one field for its computation: today.
This field has a class type and therefore the method delegates a task to
the contained class. Here, it obviously delegates the task of computing the
113
Section 12
114
115
display is a small circle and thus has an area, but the point itself doesnt.
ProfessorJ:
implements is
not Javas
Section 12
116
??? mmm();
}
// a dot
class Dot
implements IShape {
CartPt loc;
??? nnn() {
. . . this.x . . . this.y . . .
}
// a square
class Square
implements IShape {
CartPt loc;
int size;
// a circle
class Circle
implements IShape {
CartPt loc;
int radius;
Dot(CartPt loc) {
// omitted
}
Square(CartPt loc,
int size) {
// omitted
}
Circle(CartPt loc,
int radius) {
// omitted
}
??? mmm() {
. . . this.loc.nnn() . . .
}
??? mmm() {
. . . this.loc.nnn() . . .
. . . this.size . . .
}
??? mmm() {
. . . this.loc.nnn() . . .
. . . this.radius . . .
}
Figure 46: Classes for geometric shapes with methods and templates
In Square, we have two different fields: loc and size. The former is dealt
with like loc in Dot; the latter, size, is just an int. Therefore we just add this.
size to the template, without any schematic method call. Convince yourself
that the design recipe suggests a similar treatment of mmm in Circle.
117
ShapeExamples() { }
The result for Dot says that its area is 0.0. For Squares, we naturally just
square the size of the side, and for Circles we multiply the square of the
radius with . Note how the checks are formulated with a tolerance.
Using the template from figure 46 and the examples, we easily obtain
the three concrete methods:
ProfessorJ:
Testing with doubles
Section 12
118
inside of Dot :
double area() {
return 0;
}
inside of Square :
double area() {
return this.size this.size;
}
inside of Circle :
double area() {
return
(Math.PI
this.radius
this.radius);
}
Intuitively, the location of each shape plays no role when you compute their
area, and dropping the selector expressions (this. loc.nnn()) confirms this
intuition.
The only interesting aspect of testing these methods concerns the comparison of the expected value with the computed value. Recall that Javas
type double represents a discrete collection of rational numbers on the number line, i.e., not all rational numbers, and that computations on these numbers is inherently inaccurate.
The distance problem: Except for Dots, shapes consist of many different
points, so just as for Rectangles in section 11, we take this problem to mean
that the method computes the distance between the origin and the closest
point. Furthermore, lets assume that the entire shape is visible on the canvas.16 From this, we get a purpose statement and a signature:
inside of IShape :
// to compute the distance of this shape to the origin
double distTo0();
For the construction of examples, we re-use the inputs from the area problem because they have easy to compute distances:
check dot.distTo0() expect 5.0
check squ.distTo0() expect 5.0
check cir.distTo0() expect 11.0
The first two expected results are obvious. The distance between a Dot
and the origin is the distance between the Dots location and the origin; the
distance between the square and the origin is the distance between the topleft corner of the Square and the origin. The third one is similar, but while we
still compute the distance between the center of the Circle and the origin,
we must also subtract the radius from this number. After all, the points on
the circle are closer to the origin than its center.
16 Okay,
we are not only reading the problem but also simplifying it. Can you figure out
what to do when we dont make this assumption?
119
Since all methods must compute the distance between their loc field
and the origin, it makes sense to refine the template in CartPt into a distTo0
method, too:
inside of CartPt :
// to compute the distance of this point to the origin
double distTo0() { . . . }
// Functional Examples:
check (new CartPt(4, 3)).distTo0() expect 5.0
check (new CartPt(12, 5)).distTo0() expect 13.0
The functional examples are naturally adapted from the previous ones.
At this point, you can either finish the method in CartPt or those in Dot,
Square, and Circle. We start with the method in CartPt:
inside of CartPt :
// to compute the distance of this point to the origin
double distTo0(){
return Math.sqrt((this.x this.x) + (this.y this.y));
}
The method computes the distance of a point to the origin in the usual
fashion (see page 11). Since this method doesnt rely on any other method
in our classes, you can test it immediately. Do so.
Now that we have a distance method for Cartesian points, we can easily
complete the three methods in the shape classes:
inside of Dot :
double distTo0() {
return
this.loc.distTo0();
inside of Square :
double distTo0() {
return
this.loc.distTo0();
}
inside of Circle :
double distTo0() {
return
this.loc.distTo0()
this.radius;
}
All three delegate the task of computing the distance to the origin to the
appropriate method for loc. The method in Circle performs an additional
computation; the others just pass on the result of the computation in CartPt.
The point location problem: The third problem requests a method that
can find out whether some point falls within the boundaries of a shape:
inside of IShape :
// is the given point within the bounds of this shape?
boolean in(CartPt p);
Section 12
120
A method like in is useful when, among other situations, you are designing
a program that must determine whether a mouse click is within a certain
region of a canvas.
Given that there are three classes that implement IShape and exactly two
distinct outcomes, there are three pairs of test cases:
1. Conceptually, a CartPt is within a Dot if the former is equal to the
latters location:
IShape dot = new Dot(new CartPt(100, 200));
check dot.in(new CartPt(100, 200)) expect true
check dot.in(new CartPt(80, 220)) expect false
2. Deciding whether a point is within Square is difficult if you are looking at program text only. It is therefore good practice to translate
examples into graphical figures on grid paper and to check how the
dots relate to the shape. Take a look at these two examples:
check
new Square(new CartPt(100, 200), 40).in(new CartPt(120, 220))
expect true
check
new Square(new CartPt(100, 200), 40).in(new CartPt(80, 220))
expect false
Draw these two situations and confirm the expected results.
Note: This is, of course, one more situation where we suggest that
you interpret data as information.
3. For Circles the given point is inside the circle if distance between the
point and the center is less than the radius:
check new Circle(new CartPt(0, 0), 20).in(new CartPt(4, 3))
expect true
check new Circle(new CartPt(0, 0), 10).in(new CartPt(12, 5))
expect false
Recall that this kind of knowledge is domain knowledge. It is best to acquire as much basic domain knowledge as you can from courses and books;
otherwise you have to find domain experts and work with them.
As usual, thinking through these examples provides hints on how to go
from the template to the full definition. Lets look at Dot first:
121
inside of Dot :
boolean in(CartPt p) {
. . . this.loc.nnn() . . .
}
The template reminds us that we may have to design a method for CartPt
to complete the definition of in. The examples suggest that the desired
method compares p and loc; if they are the same, the answer is true and
otherwise it is false. From these two ideas, it is natural to put a same method
for CartPt on the wish list and to define in:
inside of Dot :
boolean in(CartPt p) {
return this.loc.same(p);
}
Here are three drawings that represent the question whether a given
point is within a Square:
t
( xtl , ytl )
( xtl , ytl )
t( x, y)
size
( xtl , ytl )
t( x, y)
size
t( x, y)
The leftmost picture depicts the actual question. Visually it is obvious that
the dot is within the square, which is determined by the top-left corner and
the size of the square. To do so via a computation, however, is complicated.
The basic insight is that to be inside of the square means to be between two
pairs of lines.
The picture in the middle and on the right show what this means graphically. Specifically, in the middle we see the top and bottom line and a vector that indicates what the distance between them is. The dashed line from
the given point to the vector explains that its y coordinate has to be between
the y coordinate of the top-left corner and the y coordinate of the line of the
bottom; the latter is ytl + size. Similarly, the picture on the right indicates
how the x coordinate of the given point has to be between the x coordinate
of the top-left corner and the x coordinate of the rightmost line; again the
latter is size pixels to the right of the former.
Lets look at the template and see how we can translate this analysis of
the examples into code:
Section 12
122
inside of Square :
boolean in(CartPt p) {
. . . this.loc.nnn() . . . this.size . . .
}
The adapted template on the left contains two selector expressions, reminding us of loc and size. From the example we know that both play a role.
From the former we need the coordinates. From those and size, we get the
coordinates of the parallel lines, and the coordinates of p must be in between. Furthermore, because we need to check this betweeness twice,
the one task, one functionguideline implies that we put between on our
wish list:
// is x in the interval [lft,lft+wdth]?
boolean between(int lft, int x, int wdth)
Assuming the wish is granted, finishing the definition of in is straightforward:
inside of Square :
boolean in(CartPt p) {
return this.between(this.loc.x,p.x,this.size)
&& this.between(this.loc.y,p.y,this.size);
}
This brings us to Circle:
inside of Circle :
boolean in(CartPt p) {
. . . this.loc.nnn() . . . this.radius . . .
}
Given the examples for Circle and the experience with the previous
cases, it is now almost easy to go from the template to the full definition.
As we agreed above, if the distance of the given point is less than or equal
to the radius, the point is within the circle. This statement and the template
suggest a last addition to the wish list, namely distanceTo for CartPt:
inside of Circle :
boolean in(CartPt p) {
}
Since we are proceeding in a top-down fashion this time, we cant test
anything until we empty the wish list. It contains three methods: same for
CartPt; between for Square; and distanceTo also for CartPt.
123
Section 12
124
r
More concretely, consider the circle on the left with radius r. On the
right, the same circle comes with its bounding box, whose width and
height are 2 r and whose top-left corner is one radius removed from
the center of the circle in both directions.
In short, the bounding box of any one of our shapes is a square.
Lets use this problem analysis to refine the template from figure 46.
First we create a header and a purpose statement in IShape from mmm:
inside of IShape :
// compute the bounding box for this shape
Square bb();
It is somewhat unusual that the return type of the method is Square, one of
the classes implementing IShape, but this just reflects the observation that,
in our case, the bounding boxes are just squares.
Second, we make up some examples, one per concrete shape:
check dot.bb() expect new Square(new CartPt(100, 200),1)
check squ.bb() expect squ
check cir.bb() expect new Square(new CartPt(10,3), 4)
The first two cases are straightforward. For the last one, draw the given situation on grid paper and determine for yourself why the expected answer
is correct.
Our discussion of the problem and the examples make it easy to define
the methods in Dot and Square:
inside of Dot :
Square bb() {
return new Square(this.loc,1);
}
inside of Square :
Square bb() {
return this;
}
125
inside of Circle :
inside of Circle :
Square bb() {
Square bb() {
. . . this.loc.nnn(. . . ) . . .
return
. . . this.radius . . .
new Square(this.loc.translate( this.radius),
}
2 this.radius);
}
In geometry, this operation on points (shapes actually) is called a translation. We therefore put translate on our wish list:
inside of CartPt :
// create a point that is delta pixels (up,left) from this
CartPt translate(int delta)
and wrap up the definition as if we had this new method.
Defining translate is actually easy:
inside of CartPt :
// create a point that is delta pixels (up,left) from this
CartPt translate(int delta) {
return new CartPt(this.x delta, this.y delta);
}
The methods primary argument is a CartPt. Hence, its template contains
the usual ingredients: this.x and this.y, in addition to the parameter. Furthermore, the purpose statement tells us exactly what to do: subtract delta
from this.x and this.y. Still, it would be best to follow the design recipe and
to create examples and tests now.
Figures 47 and 48 collect all the code fragments for IShape, Square, and
CartPt into complete class definitions.
Exercises
Exercise 12.1 Collect all fragments of Dot and Circle and complete the class
hierarchy in figures 47 and 48. Also collect the examples and build a working test suite for the hierarchy.
Exercise 12.2 Revise the class diagram in figure 12 so that it matches the
actual definitions in this section.
Exercise 12.3 Design an extension for the classes in figures 47 and 48 that
deals with isosceles right triangle. Assume the right angle is always in the
lower right corner and that the two sides adjacent to the right angle are
Section 12
126
interface IShape {
// to compute the area of this shape
double area();
double area() {
double distTo0() {
return this.loc.distTo0();
return
this.between(this.loc.x, p.x, this.size)
&&
this.between(this.loc.y, p.y, this.size);
}
Square bb() {
return this;
}
always parallel to the two axes. The extension should cope with all the
methods in IShape.
Remember your first design step is to develop a data representation for
these triangles. Two obvious representation come to mind: one just uses
the three points and the other one uses a representation similar to the one
for squares in this section. Explore both with examples before you design
the rest of the program. Use examples to justify your design choice.
127
class CartPt {
int x;
int y;
CartPt(int x, int y) { . . . // omitted . . . }
// to compute the distance of this point to the origin
double distTo0(){
return
Math.sqrt((this.x p.x) (this.x p.x) + (this.y p.y) (this.y p.y));
Section 13
128
An error in Java is an exception. To make life simple, ProfessorJ provides a method with the name Util.error, which consumes a String and then
signals an error:
inside of Dot :
double area() {
return Util.error("end of the world");
}
Thus, if you were to evaluate new Dot(new CartPt(10,22)).area() for this
version of Dot, the evaluation would terminate, display "end of the world",
and highlight the above expression.
We will deal with signaling errors in constructors later.
129
Room
int x
int y
IShape a
IShape b
IShape c
IShape
Dot
Square
Circle
CartPt loc
CartPt loc
int size
CartPt loc
int radius
CartPt
int x
int y
(this.width this.height);
Section 13
130
covers. . . .
For simplicity, assume that the program deals with exactly three pieces of
furniture and with rectangular rooms. For the graphical presentation of the
model, assume that the furniture is represented as IShapes, i.e., a Dot (say, a
floor lamp), a Square (say, a chair), or a Circle (say, a coffee table).
The problem statement suggests a class with five fields: the rooms
width, its height, and its three pieces of furniture. Even though we cant
know which three pieces of furniture the interior designers will place in
the room, we can use IShape as the type of the three fields because it is the
type of the union of these three classes.
Equipped with a data representation for a room, we can turn to the task
of designing the method that computes the ratio of the furnitures area to
the rooms area:
inside of Room :
// the ratio of area covered by furniture in this room
double covered() {
. . . this.a . . . this.b . . . this.c . . . // all IShape
. . . this.width . . . // int
. . . this.height . . . // int
}
The template reminds us of the five fields. Making up examples is easy,
too: if the room is 10 by 10 feet and the three pieces of furniture each cover
20 square feet, the result of covered ought to be .6. The example and its
explanation suggest the following expression for the method body:
( this.a.area() + this.b.area() + this.c.area() )
/
(this.width this.height)
In other words, the method computes and adds up the three areas covered
by the furniture and then divides by the product of height and width, the
area of the room.
Each gray-shaded expression is a method invocation that calls area on
an object of type IShape. The method is specified in the interface and thus
all implementing classes must support it. The question is how an objectoriented programming language evaluates expressions such as these or
how does it decide which area method to use.
Suppose you play interior designer and create this Room object:
131
The gray-shaded fragments in each line are evaluated. The transition from
the first line to the second explains that types dont matter during an evaluation. The shaded invocation of area of a is replaced with an invocation
to a concrete object: new Dot(. . . ). And at this point, it is completely clear
which area method is meant. The same is true in the transitions from line 3
to line 4 and line 5 to line 6, except that in those cases area from Square and
Circle are used, respectively.
Thus what really matters is how an object is created, i.e., which class
follows new. If the given IShape is an instance of Dot, the invocation of
area picks Dots area; if it is Square, its Squares version of area; and if it is
Circle, the evaluation defers to the definition of area in Circle. Afterwards,
we continue to replace parameters (also called identifiers) by their values
and proceed with the evaluation of arithmetical expressions as we know it
from primary school or How to Design Programs.
The mechanism of picking a method out of several based on its class is
called POLYMORPHIC METHOD DISPATCH. In this context, the word polymorphic refers to the fact that any class that implements IShape supplies a
method called area but each of the method definitions is unique. No conditionals or checks are written down in the program; instead the programming language itself chooses.
Section 13
132
Java, class plays both the role of a type and data label for method dispatch.These
two roles are related but they are not the same. Try not to confuse them.
133
fragment weeks, months or years after you first wrote them. It is just natural that people fail to respect contracts in such situations.
One purpose of types is to help overcome this problem. You write them
down explicitly so that others can read them. In contrast to informal contracts, types must obey the rules of the programming languages and they
are checked before you run the program. Indeed, you cant run the program if the type check fails. Other programmers (including an older you)
can then read these types with the knowledge that their use is consistent
with the languages rules.
Consistency for types is similar to consistency of a programs organization with the languages grammar, something you know from How to
Design Programs. If you write down a define with four pieces, something is
wrong, and DrScheme signals a syntax error. In the same spirit, Java checks
a programs use of types once it has checked that it obeys the grammar of
the language. Javas type checker ensures that the uses of field identifiers
and method parameters match their declared types. From this information,
it computes a type for each expression and sub-expression in your program
and always matches specified types with actual types:22
int maxLength(String s) {
if ( s.length() > 80 2 ) {
1
return s.length() ; }
3
else {
return 80; }
The gray-shaded expression with subscript 1 has type int, as does the grayshaded expression with subscript 2. Since the primitive operator < compares two ints and then produces a boolean, it is acceptable to use the comparison expression as the test component of an if statement. The grayshaded expression labeled with 3 is the same as the expression labeled 1.
Thus, no matter which of the two returns is used, the if statementand
thus the method bodyalways returns an int, just like the method signature claims. Hence the types match and the method signature correctly
describes the data that the method consumes and produces.
22 In
sound type systems, such as Javas, type checking also implies that the result of an
unsound type systems, such as the one of C++, this is not the case. While such unsound
systems can still discover potential errors via type checking, you may not rely on the type
checkers work when something goes wrong during your programs evaluation.
Section 13
134
You can also ask in this context why expression 1 has type int. In this
case, we know that s has type String. According to figure 37, the length
method consumes a String and produces an int. In other words, the type
checker can use a methods signature independently of its body to check
method calls.
As the type checker performs these checks for your program, it may
encounter inconsistencies. For example, if we write
int maxLength(String s) {
if (s > 80)
return s.length();
else
return 80;
}
s once again has type String. Its use with > (in the gray-shaded expression)
conflicts with the type specification for the operator, which says that the
operands have to be of type int. Your Java implementation therefore signals
a type error and asks you to take a closer look at your use of s. In this particular case, we know for sure that comparing a string to a number wouldnt
work during the evaluation and trigger an error. (Try it in DrScheme.) In
general, you should think of a type checker as a spell checker in your word
processor; when it finds a spelling error, the spelling is wrong, inappropriate and leads to difficulties in understanding, or intended and the spell
checker is too simplistic to discover this rare scenario.
While spell checkers do find some basic mistakes, they also miss problems. Similarly, just because your program passed the type checker, you
should not assume that it is correct. Do you remember how often your
spell checker agreed that there was the correct spelling when you really
meant their in your essays? As you proceed, keep in mind that grammarand type-checking your programs eliminates errors at the level of typos
and ill-formed sentences.23 What they usually do not find are flaws that
are comparable to problems with your chain of reasoning, the inclusion of
unchecked statistics, etc. To avoid such problems, you must design your
programs systematically and stick to a rigorous design discipline.
23 Type systems in conventional languages cant even check some of our simple informal
contracts. For example, if a function consumes a number representing a width and produces an area, we may write PositiveNumber PositiveNumber. Type systems usually
do not include such subsets of numbers.
135
Section 14
136
...
}
Canvas
int width
int height
boolean show()
boolean close()
boolean drawCircle(Posn, int, IColor)
boolean drawDisk(Posn, int, IColor)
boolean drawLine(Posn, int, int, IColor)
boolean drawString(Posn, String)
...
Again, the expression produces true if the drawing action succeeds; if not,
the computer will signal an error.
Exercises
137
Exercise 14.1 Use the libraries you have come to know (colors, draw, geometry) to draw (1) a box-and-circles car, (2) a match-stick man, and (3) a
house in ProfessorJs interactions window.
Exercise 14.2 Develop the class HouseDrawing. Its constructor should determine the size of the house (width, height) and Canvas. Since you havent
encountered this dependence, we provide the basics:
class HouseDrawing {
int width;
int height;
Canvas c;
IColor roofColor = new Red();
IColor houseColor = new Blue();
IColor doorColor = new Yellow();
HouseDrawing(int width, int height) {
this.width = width;
this.height = height;
this.c = new Canvas(width, height);
}
...
}
As always, the constructor for HouseDrawing contains one equation per
field (without initialization equation) but it lacks a parameter for the
Canvas. Instead, the canvas is constructed with the help of the other parameters, width and height. This is one way in which defining your own
constructor is superior to having defined it automatically.
The class should also come with a draw method whose purpose it is to
draw an appropriately sized house onto the canvas. In other words, the
houses measurements should depend on the width and height fields.
We suggest you start with a house that has a red, rectangular roof; a
somewhat smaller blue, rectangular frame; a yellow door, and a couple of
yellow windows. Once you can draw that much, experiment some more.
Now suppose you are to add a method show to the Room for drawing the
room. More precisely, the method should use a canvas to visually present
the room and the furniture inside the room. Since drawing should happen
on a canvas, the Room class needs a Canvas in addition to the show method:
Section 14
138
}
We have chosen to create a canvas that is as large as the room itself. Just
as in exercise 14.2, the Canvas is instantiated in the constructor because it
depends on the values of width and height, but it is not a parameter of the
constructor itself.
The design of show follows the design recipe, but requires one extra
thought. Before it can display the room, it must show the canvas, i.e., it
must invoke cs show method:
inside of Room :
// show the world (the room) with its furniture
boolean show() {
return this.c.show()
&& this.a.draw(. . . ) && this.b.draw(. . . ) && this.c.draw(. . . );
}
Then it delegates the tasks of drawing the three pieces of furniture to an
imaginary method draw in IShape. As you can see, the second like of the
template strictly follows from the design recipe.
The imaginary part means, of course, that we are adding this method
to our wish list. In this case, the wish can go directly into the IShape interface:
inside of IShape :
// draw this shape into canvas
boolean draw(Canvas c);
The Canvas parameter is needed because the draw method needs access to
it but the Canvas is only a part of the Room class. In other words, the show
method from Room must communicate c, the Canvas, to the draw methods
as an argument.
There is no true need for functional examples. We know that a draw
method in Square should draw a square at the appropriate position and of
139
the appropriate proportions. The same is true for Circles. Of course, thinking about examples does reveal that drawing a Dot presents the special-case
problem again; lets just settle for drawing a disk with radius 3 to make it
visible.
For the template step, we can reuse the template from figure 46.
inside of Dot :
inside of Square :
inside of Circle :
boolean
boolean
boolean
draw(Canvas c) {
draw(Canvas c) {
draw(Canvas c) {
. . . this.loc.nnn() . . . . . . this.loc.nnn() . . . . . . this.loc.nnn() . . .
}
. . . this.size . . .
. . . this.radius . . .
}
}
The first expression in each of these templates suggests that we can wish for
a new method for CartPt, which is where loc comes from. If loc were a Posn,
the template would translate itself into method bodies. All it would take is
an invocation of, say, drawCircle on the position, the radius, and some color.
Put differently, we have a choice with two alternatives. We can either
replace CartPt with Posn throughout our existing program or we can equip
CartPt with a method that creates instances of Posn from instances of CartPt.
Normally, the reuse of library classes is preferable; here we just provide the
method because it is so straightforward and because it demonstrates how
to bridge the small gap between the library and the rest of the code:
inside of Dot :
inside of Square :
inside of Circle :
boolean
boolean
boolean
draw(Canvas c) {
draw(Canvas c) {
draw(Canvas c) {
return
return
return
c.drawDisk(
c.drawRect(
c.drawCircle(
this.loc.toPosn(),
this.loc.toPosn(),
this.loc.toPosn(),
1,
this.size,
this.radius,
new Green());
this.size,
new Red());
}
new Blue());
}
}
Whats left to do is to design toPosn in CartPt:
inside of CartPt :
Posn toPosn() {
return new Posn(this.x, this.y);
}
Section 14
140
Exercises
Exercise 14.3 Complete the definition of the Room class.
Exercise 14.4 Add isosceles right triangles to the collection of furniture
shapes (see also exercise 12.3).
Exercise 14.5 Modify show in Room so that it also draw a 20-point, black
margin.
141
Section 15
142
143
interface ILog {
??? nnn();
}
??? nnn() {
...
}
}
class Entry {
Date d;
double distance; // miles
int duration; // minutes
String comment;
class Date {
int day;
int month;
int year;
Date(int day, int month, int year) {
. . . // omitted . . .
}
Entry(Date d,
double distance,
int duration,
String comment) {
. . . // omitted . . .
??? mmm() {
. . . this.d.lll() . . .
. . . this.distance . . .
. . . this.duration . . .
. . . this.comment . . .
}
??? nnn() {
. . . this.fst.mmm() . . .
. . . this.rst.nnn() . . .
}
??? lll() {
. . . this.day . . .
. . . this.month . . .
. . . this.year . . .
}
Section 15
144
145
class CompositeExamples {
Date d1 = new Date(5, 5, 2003);
Date d2 = new Date(6, 6, 2003);
Date d3 = new Date(23, 6, 2003);
Entry e1 = new Entry(d1, 5.0, 25, "Good");
Entry e2 = new Entry(d2, 3.0, 24, "Tired");
Entry e3 = new Entry(d3, 26.0, 156, "Great");
inside of ILog :
// to compute the total number of miles recorded in this log
double miles();
In addition, you can now rename the methods in MTLog and ConsLog.
For the functional examples, we use the sample objects from our original discussion of a runners log: see figure 52, where they are repeated.
Invoking the miles method on the ILogs produces the obvious results:
check l1.miles() expect 0.0 within .1
check l2.miles() expect 5.0 within .1
check l3.miles() expect 8.0 within .1
check l4.miles() expect 34.0 within .1
We use the examples to design each concrete method, case by case:
1. The examples show that in MTLog the method just returns 0.0. A log
with no entries represents zero miles.
2. The template for ConsLog contains two expressions. The first says that
we can compute with fst, which is an instance of Entry. The second
one computes this.rst.miles(). According to the purpose statement in
ILog, this expression returns the number of miles in the rst log that is
included in this instance of ConsLog. Put differently, we can just add
the number of miles in the fst Entry to those from rst.
Section 15
146
Using this problem analysis, it is easy to write down the two methods:
inside of MTLog :
double miles() {
return 0;
}
inside of ConsLog :
double miles() {
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 in MTLog and
the one from the second branch is in the method for ConsLog, which is just
as it should be. The conditional itself is invisible in the object-oriented program, just as described on page 13.1:
Often a runner doesnt 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 of Entry.
Put differently, the additional method consumes a runners log and two
integers, representing a month, and a year. It produces a runners logof
one months worth of entries:
inside of ILog :
// to extract those entries in this log for the given month and year
ILog oneMonth(int month, int year);
check l1.oneMonth(6, 2003) expect l1
check l3.oneMonth(6, 2003) expect new ConsLog(e2, MTLog)
check l3.oneMonth(6, 2003)
expect new ConsLog(e3, new ConsLog(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
147
log. The second one shows that extracting the June entries from l2 gives
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 for MTLog. The design of the
concrete method for ConsLog requires a look at the template to remind ourselves 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 with fst, i.e., instances of Entry. Specifically it suggests that we may
wish to design a separate method for computing some value about an instance of Entry. In this case, Entry needs a method that determines whether
an instance belongs to some given month and year, because the oneMonth
method should include fst only if it belongs to the given month and year.
To avoid getting distracted, we add an entry on our wish list:
inside of Entry :
// was this entry made in the given month and year?
boolean sameMonthAndYear(int month, int year) { . . . };
But before we design this method, lets finish oneMonth first.
Assuming that oneMonth is designed properly and works as requested,
we can finish the method in ConsLog easily:
inside of MTLog :
ILog oneMonth(int m, int y) {
return new MTLog();
}
inside of ConsLog :
ILog oneMonth(int m, int y) {
if (this.fst.sameMonthAndYear(m, y)) {
return
new
ConsLog(
this.fst,
this.rst.oneMonth(m, y)); }
else {
return this.rst.oneMonth(m, y); }
}
Section 15
148
is false, the result is whatever oneMonth extracted from rst. If it is true, fst is
included in the result; specifically, the method creates a new ConsLog from
fst and whatever oneMonth extracts from rst.
With the methods for MTLog and ConsLog 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 of Entry :
boolean sameMonthAndYear(int month, int year) {
. . . this.d.lll() . . . this.distance . . . this.duration . . . this.comment . . .
}
This implies that the method should calculate with month, year, and d, the
Date in the given Entry. The suggestive method call this.d.lll() tells us that
we can delegate all the work to an appropriate method in Date. Of course,
this just means adding another item to the wish list:
inside of Date :
// is this date in the given month and year?
boolean sameMonthAndYear(int month, int year) { . . . }
Using wishful thinking gives us the full definition of sameMonthAndYear:
inside of Entry :
boolean sameMonthAndYear(int month, int year) {
The one thing left to do is to design sameMonthAndYear for Date. Naturally, we start with a refinement of its template:
inside of Date :
boolean sameMonthAndYear(int month, int year) {
. . . 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.
month and year with this.year:
inside of Date :
// is this date in the given month and year?
boolean sameMonthAndYear(int month, int year) {
return (this.month == month) && (this.year == year);
}
149
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 experience, we recommend that you develop and test such examples now.
Exercises
Exercise 15.1 Collect all the pieces of oneMonth and insert the method definitions 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 runners 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 hierarchy 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 particular 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 runners 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 hierarchy. Assume that the method produces 0 if 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. . . .
Does the template stay the same?
Section 15
150
inside of ConsLog :
ILog sortByDist() {
. . . this.fst.mmm() . . .
. . . this.rst.sortByDist() . . .
}
The method templates suggest how to design both methods. For sortByDist in MTLog, the result is the empty log again. For sortByDist in ConsLog,
the method call
this.rst.sortByDist()
produces a sorted list of all entries in the rest of the log. That means we
only need to insert the first entry into the sorted version of rst to obtain the
sorted log that corresponds to the given one.
151
Following our wish list method, we faithfully add this insert method
to our list:
inside of ILog :
// insert the given entry into this (sorted) log
ILog insertDist(Entry e);
The difference between the previous example and this one is that we are
adding this wish list item to the very same class (hierarchy) for which we
are already designing sortByDist.
Now we can use insertDist in sortByDist:
inside of MTLog :
ILog sortByDist() {
return this;
}
inside of ConsLog :
ILog sortByDist() {
return this.rst.sortByDist().insertDist(this.fst);
}
Specifically, the method first invokes sortByDist on rst and then invokes
insertDist on the result. The second argument to insertDist is this.fst, the
first Entry in the given log.
Now that were done with sortByDist, we turn to our wish list, which
contains insertDist in ILog. We immediately move on to the development of
a good set of functional examples, starting with a new data example that
contains three entries:
ILog l5 =
new ConsLog(new Entry(new Date(1,1,2003), 5.1, 26, "good"),
new ConsLog(new Entry(new Date(1,2,2003), 4.9, 25, "okay"),
new MTLog()))
Because the addition of another instance of Entry to l5 can take place at
three distinct places, we develop three distinct examples:
1. The first example shows that the given Entry might end up in the
middle of the given log:
check l5.insertDist(new Entry(new Date(1,3,2003), 5.0, 27, "great"))
expect
new ConsLog(new Entry(new Date(1,1,2003), 5.1, 26, "good"),
new ConsLog(new Entry(new Date(1,3,2003), 5.0, 27, "great"),
new ConsLog(new Entry(new Date(1,2,2003), 4.9, 25, "okay"),
new MTLog())))
2. In the second case, the given Entry is the first Entry of the resulting
log:
Section 15
152
The method on the left must return a log with one Entry because the purpose statement promises that the given Entry and all the Entrys in the given
log show up in the result. Since the given log is an instance of MTLog, the
result must be the log that consists of just the given Entry.
The method on the right must distinguish two cases. If the distance
in the given Entry is larger than the distance in fst, it is larger than all the
distances in the given log, and therefore the given Entry must show up at
153
the beginning of the result. If the distance in the given Entry is less than
(or equal to) the distance in fst, the recursive method call inserts the given
Entry in rst and the method just adds fst to the result of this recursive call.
Exercises
Exercise 15.4 Suppose the requirements for the program that tracks a runners log includes this request:
. . . The runner would like to see the log with entries ordered
according to the pace computed in minutes per mile in each run,
from the fastest to the slowest. . . .
Design this sorting method.
Section 15
154
interface IComposite {
??? nnn();
}
class Square
implements
IComposite {
CartPt loc;
int size;
class Circle
implements
IComposite {
CartPt loc;
int radius;
class SuperImp
implements
IComposite {
IComposite bot;
IComposite top;
Square(CartPt loc,
int size){
this.loc = loc;
this.size = size;
}
Circle(CartPt loc,
int radius){
this.loc = loc;
this.radius = radius;
}
SuperImp(
IComposite bot,
IComposite top) {
this.bot = bot;
this.top = top;
}
??? nnn() {
. . . this.loc . . .
. . . this.size . . .
}
??? nnn() {
. . . this.loc . . .
. . . this.radius . . .
}
??? nnn() {
. . . this.bot.nnn() . . .
. . . this.top.nnn() . . .
}
While this formulation of the problem is quite different from the original,
rather plain formulation on page 118 for the collection of basic shapes, it
is easy to recognize that they are the basically the same. The origin is the
target, and the distance to the origin is the distance to the target. If we
continue to assume that the shape is entirely on the canvas, the signature
and the purpose statement carry over from the original problem to this one:
inside of IComposite :
// to compute the distance of this shape to the origin
double distTo0();
Indeed, the concrete methods for Square and Circle also remain the same.
The difference is the concrete method for SuperImp, which must compute
the distance of an entire combination of shapes to the origin.
For the classes Square and Circle the expected results are computed the
same was as those we saw earlier: see testS1, testS2, testC1, and testC2 in fig-
155
class CompositeExamples {
IComposite s1 = new Square(new CartPt(40, 30), 40);
IComposite s2 = new Square(new CartPt(120, 50), 50);
IComposite c1 = new Circle(new CartPt(50, 120), 20);
IComposite c2 = new Circle(new CartPt(30, 40), 20);
IComposite u1 = new SuperImp(s1, s2);
IComposite u2 = new SuperImp(s1, c2);
IComposite u3 = new SuperImp(c1, u1);
IComposite u4 = new SuperImp(u3, u2);
boolean testS1 = check s1.distTo0() expect 50.0 within .1;
boolean testS2 = check s2.distTo0() expect 80.0 within .1;
boolean testC1 = check c1.distTo0() expect 110.0 within .1;
boolean testC2 = check c2.distTo0() expect 30.0 within .1;
CompositeExamples() { }
ure 54. The instances of SuperImp make up the interesting examples. Given
that a SuperImp contains two shapes and that we wish to know the distance
of the closer one to the shape, we pick the smaller of the two distances:
check u1.distTo0() expect 50.0 within .1
check u2.distTo0() expect 30.0 within .1
check u3.distTo0() expect 50.0 within .1
check u4.distTo0() expect 30.0 within .1
The distance of u2 to the origin is 30.0 because the Square s1 is 50.0 pixels
away and the Circle c2 is 30.0 pixels away. Convince yourself that the other
predicted answers are correct; draw the shapes if you have any doubts.
Our reasoning about the examples and the template for SuperImp imply
that the method just computes the distance for the two shapes recursively
and then picks the minimum:
inside of SuperImp :
double distTo0(){
return Math.min(this.bot.distTo0(), this.top.distTo0());
}
As suggested by its name, Math.min picks the smaller of two numbers.
Here is a related problem (from the same contest):
Section 15
156
. . . Assuming the shapes represent those points that are reachable with anti-aircraft missiles, the commanding officer wishes
to know whether some point in the Cartesian space falls within
the boundaries of the formations outline. . . .
If you prefer a plainer problem statement, see page 119 for the analogous
problem for basic shapes.
Clearly the methods for Square and Circle can be used as is. The purpose statement and header from IShape can also be transplanted into the
new interface:
inside of IComposite :
// is the given point within the bounds of this shape
boolean in(CartPt p);
inside of CompositeExamples :
check u1.in(new CartPt(42,42)) expect true
check u2.in(new CartPt(45,40)) expect true
check u2.in(new CartPt(20,5)) expect false
The examples illustrate that being within the boundary means being
within one or the other shape of a SuperImp.
And again, we use the template and the examples to assemble the concrete method for SuperImp in a straightforward manner:
inside of SuperImp :
double in(CartPt p){
return this.bot.in(p) || this.top.in(p);
}
The method computes the results for both of the shapes it contains and
then checks whether one or the other works. Recall that b1 || b2 computes
whether b1 or b2 is true.
Without ado, here is a restatement of the last geometric problem for
basic shapes (see page 123):
. . . The Navy wishes to know approximately how large an area
a group of objects covers. It turns out that they are happy with
the area of the bounding box, i.e., the smallest rectangle that
contains the entire shape. . . .
Since we already have a solution of this problem for Dots, Squares, and
Circles, it looks like we just need to solve the problem for SuperImp, the
variant in the union.
157
'$
r
&%
r'$
r
&%
r '$
r
&%
The circles center is on the top-line of the square. In the central drawing,
we see the squares and the circles individual bounding boxes; the squares
bounding box is itself and the circles bounding box is the dashed square.
The right drawing shows the bounding box for the entire SuperImp shape
as a dashed rectangle; as you can see, its a rectangle proper.
The immediate conclusion of our analysis is that the bounding box of a
composite shape is a rectangle, not a square. Hence, Square can no longer
serve as the representation of bounding boxes; instead we need something
that represents rectangles in general. There are two obvious choices:
1. We can extend our shape datatype with a variant for representing
rectangles. That is, we would add a variant to the existing union that
that represents rectangles in exactly the same fashion as an instance
of Square represents a square and an instance of Circle represents a
circle.
2. We can define a class that is tailored to creating bounding boxes. Since
bounding boxes are rectangles, such a class would also represent a
rectangle. It doesnt have to implement IComposite, however, imposing fewer burdens on the design process. In particular, we do not
have to add all the methods that IComposite demands from its implementing classes.
Each has advantages and disadvantages, even without considering the context in which your classes are used. Stop to think briefly about an advantage/disadvantage for each alternative.
Here we develop a solution for the second alternative; see exercise 15.5
for the first one. As a matter of fact, we dont even commit to the fields of
the new class; all we assume for now is that it exists:
class BoundingBox { . . . }
Section 15
158
Even with that little, we can get pretty far, starting with a contract proper
for the bb method in IComposite:
inside of IComposite :
// compute the bounding box for this shape
BoundingBox bb();
The second step is to calculate out some examples, except that we dont
know how to express them with BoundingBox. We therefore write them
down in a precise but informal manner:
1. s1.bb() should produce a 40-by-40 rectangle at (40,80);
2. s2.bb() should produce a 50-by-50 rectangle at (120,50);
3. c1.bb() should produce a 40-by-40 rectangle at (30,100);
4. c2.bb() should produce a 40-by-40 rectangle at (10,20).
Not surprisingly, these rectangles are squares because they are bounding
boxes for circles and squares. Still, the descriptions illustrate how to work
out examples without knowledge about the result type.
Next we look at the bounding boxes of instances of SuperImp:
1. u1.bb() should produce a 110-by-70 rectangle at (40,30);
2. u2.bb() should produce a 70-by-50 rectangle at (10,20);
3. u3.bb() should produce a 70-by-120 rectangle at (10,20);
4. u4.bb() should produce a 70-by-120 rectangle at (10,20).
For the template step, we use the generic templates from figure 53 and
refine them for this specific problem:
inside of Square :
BoundingBox bb() {
. . . this.loc . . .
. . . this.size . . .
}
inside of Circle :
BoundingBox bb() {
. . . this.loc . . .
. . . this.radius . . .
}
inside of SuperImp :
BoundingBox bb() {
. . . this.bot.bb() . . .
. . . this.top.bb() . . .
}
159
inside of SuperImp :
BoundingBox bb() {
// compute the bounding box for top
. . . this.top.bb() . . .
// compute the bounding box for bot
. . . this.bot.bb() . . .
}
These refined purpose statements tells us that the two expressions in the
template produce bounding boxes for the respective shapes. Since it is
clearly a complex task to combine two bounding boxes into a bounding
box, we add a wish to our wish list:
inside of BoundingBox :
// combine this bounding box with that one
BoundingBox combine(BoundingBox that);
If the wish works, we can finish bbs definition:
inside of SuperImp :
BoundingBox bb() {
return this.top.bb().combine(this.bot.bb());
}
Before you continue, contemplate why combine is a method in BoundingBox.
To make progress, we need to reflect on the bb methods in Circle and
Square. Both must produce instances of BoundingBox that represent squares
or, in general, rectangles. Before we commit to a concrete definition of
BoundingBox, lets briefly discuss possible representations of rectangles:
1. In the past we have represented rectangles with three pieces of information: the anchor point, its width, and its height.
2. One obvious alternative is to represent it with the four corners. Since
we committed to have the sides of rectangles run parallel to the axes,
we actually just need two opposing corners.
3. Based on the first two alternatives, you can probably think of a mixture of others.
Before you commit to a choice in this situation, you should explore
whether the other operations needed on your class are easy to calculate.
Here we just need one: combine, which turns two rectangles into the smallest rectangle encompassing both. When you are faced with such a choice,
it helps to plan ahead. That is, you should see how easily you can calculate
Section 15
160
with examples or whether you can calculate examples at all. For this particular example, it helps to draw pictures of rectangles and how you would
combine them, i.e, surround them with one large rectangle. See the three
drawings above that explain how to get one such rectangle for one instance
of SuperImp.
Drawing such pictures tells you quickly that the combine method has
to pick the extreme left line, right line, top line, and bottom line from the
four sides. As it turns out, this insight is easy to express for the second
alternative but takes quite some work for the first. Specifically, if instances
of BoundingBox contain the coordinates of two opposing corners, combine
could, for example, use the extreme left and the extreme top coordinate for
one new corner and the extreme right and the extreme bottom coordinate
for the other. Indeed, this consideration implies that it suffices to record
these four numbers in a BoundingBox; after all, they determine the rectangle
and they allow a straightforward definition of combine.
Here is the basic idea then:
// representing bounding boxes in general
class BoundingBox {
int lft;
int rgt;
int top;
int bot;
...
// combine this bounding box with that one
BoundingBox combine(BoundingBox that) { . . . }
}
Before we design the method, though, we should formulate the examples
for the bb method for this choice to ensure we understand things properly:
inside of CompositeExamples :
boolean test1 = check s1.bb() expect new BoundingBox(40,80,30,70);
boolean test2 = check s2.bb() expect new BoundingBox(120,170,50,100);
boolean test3 = check c1.bb() expect new BoundingBox(30,70,100,140);
boolean test4 = check c2.bb() expect new BoundingBox(10,50,20,60);
boolean test5 = check u1.bb() expect new BoundingBox(40,170,30,100);
boolean tets6 = check u2.bb() expect new BoundingBox(10,80,20,70);
boolean test7 = check u3.bb() expect new BoundingBox(10,80,20,140);
boolean test8 = check u4.bb() new BoundingBox(10,80,20,140);
161
The first four examples show how to compute a bounding box for Squares
and Circles; not surprisingly, these bounding boxes represent squares in the
Cartesian plane. The fifth and sixth are for SuperImps but they are easy to
compute by hand because they just combine the bounding boxes for basic
shapes. The last two expected bounding boxes require some calculation.
For such examples, it is best to sketch the given shape on drawing paper
just to get a rough impression of where the bounding box should be.
Now that we the functional examples and a template, we can define the
methods for the basic classes, Square and Circle:
inside of Square :
BoundingBox bb() {
return
new BoundingBox(
this.loc.x,
this.loc.x+this. size,
this.loc.y,
this.loc.y+this. size);
}
inside of Circle :
BoundingBox bb() {
return
new BoundingBox(
this.loc.x this.radius,
this.loc.x + this.radius,
this.loc.y this.radius,
this.loc.y + this.radius);
}
Computing the margins for a Square is obvious. For Circles, the left margin is one this.radius to the left of the center, which is at located at this.
loc; similarly, the right margin is located one this.radius to the right of the
center. For the top and the bottom line, the method must conduct similar
computations.
There is one entry left on our wish list: combine in BoundingBox. Recall that the purpose of combine is to find the (smallest) BoundingBox that
contains this and that BoundingBox, where the latter is given as the second argument. Also recall the picture from the problem analysis. Clearly,
the left-most vertical line of the two bounding boxes is the left-most line
of the comprehensive bounding box and therefore determines the lft field
of the combined box. This suggests, in turn, that combine should compute
the minimum of this.lftthe left boundary of thisand that.lftthe left
boundary of that:
. . . Math.min(this.lft,that.lft) . . .
Before you move on: what are the appropriate computations for the pair of
right-most, top-most, and bottom-most lines?
Putting everything together yields this method definition:
Section 15
162
inside of BoundingBox :
BoundingBox combine(BoundingBox that) {
return new BoundingBox(Math.min(this.lft,that.lft),
Math.max(this.rgt,that.rgt),
Math.min(this.top,that.top),
Math.max(this.bot,that.bot))
}
Exercises
Exercise 15.5 When we discussed the design of BoundingBox, we briefly
mentioned the idea of adding a Rectangle class to the IComposite hierarchy
figure 53:
class Rectangle implements IComposite {
CartPt loc;
int width;
int height;
Rectangle(CartPt loc, int width, int height){
this.loc = loc;
this.width = width;
this.height = height;
}
??? nnn() {
. . . this.loc . . .
. . . this.width . . .
. . . this.height . . .
}
Now re-design the method bb for IComposite using this signature and
purpose statement:
inside of IComposite :
// compute the bounding box for this shape
Rectangle bb();
163
Hint: Extend Rectangle with auxiliary methods for computing the combination of bounding boxes.
Note: This exercise illustrates how a decision concerning the representation of informationthe bounding boxescan affect the design of methods, even though both design processes use the same design recipe after
the initial decision.
Exercise 15.6 Drawing shapes and their bounding boxes is a graphical way
of checking whether bb works properly. Equip all classes from this section
with a draw method and validate the results of bb via visual inspection.
Add the class Examples, which creates examples of shapes and contains a
method that first draws the bounding box for a given shape and then the
shape itself. Use distinct colors for bounding boxes and shapes.
Section 15
164
// a location on a river
class Location{
int x;
int y;
String name;
...
??? nnn() {
. . . this.x . . . this.y . . . this.name . . .
}
}
??? nnn() {
. . . this.loc.mmm() . . .
. . . this.river.nnn() . . .
}
// a river system
interface IRiver{
??? nnn();
}
// the source of a river
class Source implements IRiver {
Location loc;
...
??? nnn() {
. . . this.loc.mmm() . . .
}
??? nnn() {
. . . this.loc.mmm()
. . . this.left.nnn() . . .
. . . this.right.nnn(. . . ) . . .
}
Each source contributes 1 to the total count. For each confluence of two
rivers, we add up the sources of both tributaries. And the sources that feed
a mouth are the sources of its river.
Here are the methods, including signatures and purpose statements for
Mouth and IRiver:
inside of Mouth :
// count the number of sources
// that feed this Mouth
int sources() {
return this.river.sources();
}
165
inside of IRiver :
// count the number of sources
// for this river system
int sources();
The method for Mouth just calls the method for its river, following the containment arrow in the class diagram. Also following our design rules for
unions of classes, the method in IRiver is just a signature.
Next we define the methods for Source and Confluence:
inside of Source :
int sources(){
return 1;
}
inside of Confluence :
int sources(){
return this.left.sources() + this.right.sources();
}
The templates and the method examples suggest these straightforward definitions. You should make sure sure that these methods work as advertised
by the examples.
The next problem involves the locations that are a part of river systems:
. . . An EPA officer may wish to find out whether some location
is a part of a river system. . . .
Take a look at figure 56. It contains the refined templates for the relevant
five classes: Mouth, IRiver, Confluence, Source, and Location. Specifically,
1. the refined methods have names that are appropriate for the problem;
2. they have complete signatures;
3. and they come with purpose statements.
The Location class is also a part of the class hierarchy because the problem
statement implies that the search methods must be able to find out whether
two Locations are the same.
Our next step is to work through some examples:
check mouth.onRiver(new Location(7,5)) expect true
After all, the given location in this example is the location of the mouth
itself. Hence, we also need an example where the location is not the mouth:
check mouth.onRiver(new Location(1,5)) expect false
Section 15
166
// a location on a river
class Location {
int x;
int y;
...
...
boolean same(Location aloc) {
. . . this.x . . . aloc.x . . .
. . . this.y . . . aloc.y . . .
}
}
. . . this.loc.same(aloc) . . .
. . . this.river.onRiver(Location aloc) . . .
}
// a river system
interface IRiver{
boolean onRiver(Location aloc);
}
// the source of a river
class Source implements IRiver {
Location loc;
...
. . . this.loc.same(aloc) . . .
. . . this.loc.same(aloc)
. . . this.left.onRiver(aloc) . . .
. . . this.right.onRiver(aloc) . . .
}
}
This time, the given location is the source of t, which joins up with s to
form b, and that in turn flows into a and thus mouth itself. A complete
set of examples would contain method calls for the templates in Source,
Confluence, and Location.
The examples suggest that onRiver in Mouth checks whether the given
location is the location of the mouth or occurs on the river system:
inside of Mouth :
// does aloc occur
// along this river system?
boolean onRiver(Location aloc){
return this.loc.sameloc(aloc) ||
this.river.onRiver(aloc);
}
167
inside of IRiver :
// does aloc occur
// along this river system?
boolean onRiver();
Again, the two method calls are just those from the template; their combination via || is implied by the examples. Both follow the containment
arrows in the diagram, as suggested by the design recipe.
The methods in Source and Confluence follow the familiar pattern of recursive functions:
inside of Source :
boolean onRiver(Location aloc){
return this.loc.sameloc(aloc);
}
inside of Confluence :
boolean onRiver(Location aloc){
return this.loc.sameloc(aloc) ||
this.left.onRiver(aloc) ||
this.right.onRiver(aloc);
}
In Source, the method produces true if and only if the given location and
the location of the source are the same; in Confluence, the given location
could be on either branch of the river or it could mark the location of the
confluence.
Last but not least we must define what it means for two instances of
Location to be the same:
inside of Location :
// is this location identical to aloc?
boolean same(Location aloc) {
return (this.x == aloc.x) && (this.y == aloc.y);
}
Like the methods above, this one is also a simple refinement of its template.
Note: If we wanted to have a more approximate notion sameness, we
would of course just change this one definition. The others just defer to
Location for checking on sameness, and hence changing sameness here
would change it for every method. What you see in action is, of course, the
principle of single point of control from How to Design Programs.
Here is the final problem concerning river systems:
Section 15
168
169
add fields to the existing classes that record the length of associate segments. Still, even if we leave the overall structure of the hierarchy alone,
we face another choice:
1. for any point of interest, we can record the length of the downward
segment; or
2. for any point of interest, we can record the length of the upward segment.
Here we choose to leave the structure of the class hierarchy alone and to
record the length of a segment in its origination point (choice 1); exercise 15.9 explores the second choice.
class Mouth {
Location loc;
IRiver river;
...
interface IRiver {
??? n();
}
??? nnn() {
. . . this.loc.mmm() . . .
. . . this.river.nnn() . . .
}
class Source implements IRiver {
int miles ;
Location loc;
...
??? nnn() {
. . . this.miles . . .
. . . this.loc.mmm() . . .
}
??? nnn() {
. . . this.miles . . .
. . . this.loc.mmm()
. . . this.left.nnn() . . .
. . . this.right.nnn(. . . ) . . .
}
Figure 58: Adding the length of a river segment to the data representation
Figure 58 shows the adapted classes for a river system. The new fields
in Source and Confluence denote the length of the down-river segment that
Section 15
170
inside of IRiver :
// compute the total length of the
// waterways that flow into this point
int length();
The method in Mouth still defers to the method of its river field to compute
the result; and that latter method is just a signature, i.e., it is to be defined
in the variants of the union.
All that is left to define are the methods in Source and Confluence:
inside of Source :
boolean length(){
return this.miles;
}
inside of Confluence :
int length(){
return this.miles +
this.left.length() +
this.right.length();
}
The total length for the Source class is the value of the length. The total
length of the river flowing to a Confluence is the sum of the total lengths of
the two tributaries and the length of this river segment.
Exercises
171
Exercise 15.7 The EPA has realized that its case officers need a broader
meaning of finding locations along the river system than posed in this
section:
. . . An EPA officer may wish to find out whether some location
is within a given radius of some confluence or source on a river
system. . . .
Modify the existing onRiver method to fit this revised problem statement.
Exercise 15.8 Design the following methods for the class hierarchy representing river systems:
1. maxLength, which computes the length of the longest path through
the river system;
2. confluences, which counts the number of confluences in the river system; and
3. locations, which produces a list of all locations on this river, including
sources, mouths, and confluences.
Exercise 15.9 Design a representation of river systems such that each place
(mouth, confluence, or source) describes how long the segments are that
flow into it. Hint: For a confluence, you will need two lengths: one for the
left tributary and one for the right.
Section 15
172
IGUIComponent
TextFieldView
ColorView
String label
String label
BooleanView
OptionsView
String label
String label
ITable
IRow
MTTable
AddRow
Table rest
IRow first
ConsRow
MTRow
IRow rest
GUIComponent first
Designing Methods
173
16 Designing Methods
The purpose of a method is to produce data from the data in the given
object and the methods arguments. For that reason, the design recipe for
methodsadapted from the design recipe from How to Design Programs
for functionsfocuses on laying out all the available pieces of data as the
central step:
purpose & signature When you design a method, your first task is to clarify what the method consumes and what it produces. The method
signature specifies the classes of (additional) inputs and the class of
outputs. You should keep in mind that a method always consumes
at least one input, the instance of the class on which it is invoked.
Once you have a signature, you must formulate a purpose statement
that concisely states what the method computes with its arguments.
You do not need to understand (yet) how it performs this task. Since
the method consumes (at least) the instance of the class in which the
method is located, it is common practice to write down the purpose
statement in terms of this; if the names of parameters are useful, too,
use them to make the statement precise.
functional examples The second step is the creation of examples that illustrate the purpose statement in a concrete manner.
template The goal of the template step is to spell out the pieces of data
from which a method can compute its result. Given that a method
always consumes an instance of its class, the template definitely contains references to the fields of the class. Hint: Annotate these selector expressions with comments that explain their types; this often
helps with the method definition step.
If any of the fields have a class or interface type, remind yourself with
an appropriate expression that your method canand often must
use method calls on these objects.
Additionally, if your methods extra arguments have class or interface
types, add a schematic method call to the method parameter (p):
AType m(AClass p, . . . ) {
. . . p.lll()
}
Section 16
174
Last but not least, before you move on, keep in mind that a method
body may use other already defined methods in the surrounding class
or in the class and interaces of its parameters.
method definition Creating the method body is the fourth step in this sequence. It starts from the examples; the template, which lays out all
the available information for the computation; and the purpose statement, which states the goal of the computation. If the examples dont
clarify all possible cases, you should add examples.
tests The last task is to turn the examples into executable tests. Ideally
these tests should be evaluated automatically every time you edit the
program and get it ready to run.
The five steps depend on each other. The second step elaborates on the first;
the definition of the method itself uses the results of all preceding steps; and
the last one reformulates the product of the second step. On occasion, however, it is productive to re-order the steps. For example, creating examples
first may clarify the goal of the computation, which in turn can help with
the purpose statement. Also, the template step doesnt depend on the examples, and it is often possible and beneficial to construct a template first.
It will help a lot when methods come in bunches; in those cases, it is often
straightforward to derive the methods from the templates then.
Basic Classes
A basic class comes with a name and some properties of primitive type:
Basic
Basic
String s
int i
double d
String s
int i
double d
??? nnn() {
... this.i ...
... this.d ...
... this.j ... }
Designing Methods
175
Containment
The next case concerns a class that refers to (instances of) other classes:
Containment
Basic
Basic b
double d
String s
int i
double d
Basic
Basic b
double d
String s
int i
double d
??? mmm() {
... this.b.nnn() ...
... this.d ...
}
??? nnn() {
... this.i ...
... this.d ...
... this.j ... }
Unions
Here is the diagram for the third situation:
Section 16
176
IUnion
Basic1
Basic2
Basic3
boolean b
double d
int i
double d
int i
String s
The diagram specifies IUnion as the interface for the union of three concrete
classes: Basic1, Basic2, and Basic3. An interface such as IUnion represents a
common facade for the three classes to the rest of the program.
The template for a union of classes requires a method signature in the
interface and basic templates in the concrete variants of the union:
IUnion
Basic1
Basic2
Basic3
boolean b
double d
int i
double d
int i
String s
??? mmm() {
... this.b ...
... this.d ... }
??? mmm() {
... this.i ...
... this.d ... }
??? mmm() {
... this.i ...
... this.s ... }
The method templates for the basic classes mention all fields of the respective classes.
Designing Methods
177
IUnion
Basic1
Basic2
Basic3
String s
double d
IUnion u
A closer look at the diagram shows that this last particular example is a
composition of the containment and the union case.
Accordingly the creation of the method templates is a composition of
the actions from the preceding two cases. Here is the resulting diagram:
IUnion
Basic1
Basic2
Basic3
String s
double d
IUnion u
??? mmm() {
... this.s ... }
??? mmm() {
... this.d ... }
??? mmm() {
... this.u.mmm() ... }
The IUnion interface contains a method signature for mmm, specifying how
all instances of this type behave. The three concrete classes contain one concrete template for mmm each. The only novelty is in the template for Contain3s mmm method. It contains a method invocation of mmm on u. Since u
is of type IUnion, this recursive invocation of mmm is the only natural way
to compute the relevant information about u. After all, we chose IUnion
as us type because we dont know whether u is going to be an instance of
Atom1, Atom2, or Contain3. The recursive invocation therefore represents a
dynamic dispatch to the specific method in the appropriate concrete class.
Even though the sample diagram is an example of a self-referential class
hierarchy, the idea smoothly generalizes to complex cases, including class
hierarchies with mutual references. Specifically, an interface for a union of
classes always contains a general method signature. As we follow the (reverse) refinement arrows, we add concrete versions of the method signature to the classes. The body of the template contains one reference to each
field. If the field refers to another class in the hierarchy along a containment
Section 16
178
arrow, we turn the field reference into a method call. If, furthermore, the
reference to the other class creates a cycle in the class hierarchy, the method
invocation is recursive. Note how for a collection of classes with mutual
references along containment arrows, the design recipe creates natural mutual references among the methods.
Designing Methods
179
Section 16
180
class UFO {
Posn location;
IColor colorUFO = new Green();
AUP(int location) { . . . }
UFO(Posn location) { . . . }
Designing Methods
181
6. If the UFO has landed, the player has lost the game.
Implicitly, the problem also asks for methods that draw the various objects
onto the canvas. Here we discuss how to develop methods for drawing the
objects of the world in a canvas, for moving them, and for firing a shot. The
remaining methods are the subject of exercises at the end of the section.
Figures 60 and 61 display the classes that represent the entire world,
UFOs, AUPs, and lists of shots; the routine constructors are elided to keep
the figures concise. When you add purpose statements and signatures to
such a system of classes, start with the class that stands for the entire world.
Then you follow the containment and refinement arrows in the diagram
as directed by the design recipe. If you believe that a method in one class
requires a method with an analogous purpose in a contained class, its okay
to use the same name. These additions make up your original wish list.
Now take a look at the draw method in UFOWorld. Its purpose is to
draw the world, that is, the background and all the objects contained in the
world. The design recipe naturally suggests via the design template that
draw in UFOWorld use a draw method in UFO, AUP, and IShots to draw the
respective objects:
inside of UFOWorld :
boolean draw(Canvas c) {
. . . this.BACKG . . . this.HEIGHT . . . this.WIDTH . . .
. . . this.ufo.draw(. . . ) . . .
. . . this.aup.draw(. . . ) . . .
. . . this.shots.draw(. . . ) . . .
}
An example of a canvas for UFOWorld came with the original problem
statement (see page 56). The image shows that the world is a rectangle,
the UFO a flying saucer, and the AUP a wide horizontal rectangle with a
short vertical rectangle sticking out in the middle. Before we can define
the draw method, however, we must also recall from section 14.1 how the
drawing package works. A Canvas comes with basic methods for drawing
rectangles, circles, disk and so on. Hence, a draw method in our world must
consume an instance of Canvas and use its methods to draw the various
objects.
With all this in mind, we are now ready to define draw:
Section 16
182
inside of UFOWorld :
boolean draw(Canvas c) {
return
c.drawRect(new Posn(0,0), this.WIDTH,this.HEIGHT,this.BACKG)
&& this.ufo.draw(c)
&& this.aup.draw(c)
&& this.shots.draw(c);
}
It combines four drawing actions with &&, meaning all four must succeed
before draw itself signals success with true.
}
// the empty list of shots
class MtShots implements IShots {
Shot(Posn location) { . . . }
// draw this shot
boolean draw(Canvas c) { . . . }
// move this list of shots
Shot move(UFOWorld w) { . . . }
MtShots() { }
boolean draw(Canvas c) { . . . }
boolean draw(Canvas c) { . . . }
IShots move(UFOWorld w) { . . . }
IShots move(UFOWorld w) { . . . }
Figure 61: Shots and lists of shots with preliminary method specifications
The draw methods in AUP and UFO just draw the respective shapes. For
example, draw in UFO draws a disk and a rectangle placed on its center:
Designing Methods
183
inside of UFO :
// draw this UFO
boolean draw(Canvas c) {
return
c.drawDisk(this.location,10,this.colorUFO) &&
c.drawRect(new Posn(this.location.x 30,this.location.y 2),
60,4,
this.colorUFO);
}
Note how the calculations involve for the positions plain numbers. Following the advice from How to Design Programs, it would be better of course to
introduce named constants that the future readers what these numbers are
about. Do so!
In comparison, IShots contains only a method signature because IShots
is the interface that represents a union. Its addition immediately induces
two concrete draw methods: one in MtShots and one in ConsShots. Here are
the two templates:
inside of MtShots :
boolean draw(Canvas c) {
...
}
inside of ConsShots :
boolean draw(Canvas c){
. . . this.first.draw() . . . this.rest.draw() . . .
}
Since the purpose of the draw method is to draw all shots on a list of shots,
it is natural to add a draw method to Shot and to use this. first.draw() in
ConsShots. Like all other draw methods, these, too, consume a Canvas so
that they can use the drawing methods for primitive shapes.
Exercises
Exercise 16.1 Define the draw methods for MtShots, ConsShots, and Shot.
Remember that on screen, a Shot is drawn as a thin vertical rectangle.
Exercise 16.2 Define the draw method in AUP. As the screen shot in the
problem statement on page 56 indicates, an AUP consists of two rectangles,
a thin horizontal one with a short, stubby vertical one placed in its middle.
It is always at the bottom of the screen. Of course, your boss may require
that you can change the size of UFOWorld with a single modification, so
you must use a variable name not a number to determine where to place the
AUP on the canvas. Hint: Add a parameter to the draw methods parameter
Section 16
184
list so that it can place itself at the proper height. Does this change have any
implications for draw in UFOWorld?
Exercise 16.3 When you have solved exercises 16.1 and 16.2 combine all
the classes with their draw methods and make sure your code can draw a
world of UFOs. You may wish to use the examples from figure 34.
Once your program can draw a world of objects, the natural next step
is to add a method that makes the objects move. The purpose of move
in UFOWorld is to create a world in which all moving objects have been
moved to their next placewhatever that is. Hence, if the method is somehow called on a regular basis and, if every call to move is followed by a call
to draw, the player gets the impression that the objects move continuously.
Here is a draft purpose statement, a method signature, and a template
for the move method:
inside of UFOWorld :
// to move all objects in this world
UFOWorld move() {
. . . this.BACKG . . . this.HEIGHT . . . this.WIDTH . . .
. . . this.ufo.move(. . . ) . . .
. . . this.aup.move(. . . ) . . .
. . . this.shots.move(. . . ) . . .
}
The template reminds us that the world consists of three objects and, following the design recipe, that each of these objects also has a move method.
You might conclude that the move method for the world moves all objects
just like the draw method draws all objects. This conclusion is too hasty,
however. While the UFO and the shots are moving automatically, the AUP
moves in response to a players instructions. We therefore exclude the AUP
from the overall movement and focus only on the UFO and the shots:
inside of UFOWorld :
// to move the UFO and the shots in this world
UFOWorld move() {
return
new UFOWorld(this.ufo.move(),this.aup,this.shots.move());
}
That is, the move method creates a new UFOWorld from the existing AUP
plus a UFO and a list of shots that have been moved.
Designing Methods
185
Exercises
Exercise 16.4 Our development skipped over the example step. Use figure 34 to develop examples and turn them into tests. Run the tests when
the move methods for UFOs and IShots are defined (see below).
By following the design recipe, you have placed two methods on your
wish list: move in UFO and move in IShots. The latter is just a signature; its
mere existence, though, implies that all implementing classes must posses
this method, too. The former is concrete; its task is to create the next UFO.
This suggests the preliminary method signatures and purpose statements
in figures 60 and 61.
Lets take a step at a time and develop examples for move in UFO. Suppose the method is invoked on
new UFO(new Posn(88,11))
Given the dimensions of UFOWorld, this UFO is close to the top of the canvas and somewhat to the left of the center. Where should it be next? The
answer depends on your boss, of course.24 For now, we assume that move
drops the UFO by three (3) pixels every time it is invoked:
check new UFO(new Posn(88,11)).move()
expect new UFO(new Posn(88,14))
Similarly:
check new UFO(new Posn(88,14)).move()
expect new UFO(new Posn(88,17))
and
check new UFO(new Posn(88,17)).move()
expect new UFO(new Posn(88,20))
While this sequence of examples shows the natural progression, it also
calls into question the decision that move drops the UFO by 3 pixels every
time it is invoked. As the problem statement says, once the UFO reaches
24 Deep down, it depends on your knowledge, your imagination, and your energy. Many
computer games try to emulate physical laws. To do so, you must know your physics. Others rely on your imagination, which you can foster, for example, with readings on history,
mythology, science fiction, and so on.
Section 16
186
the ground level, the game is over and the player has lost. Translated into
an example, the question is what the expression
new UFO(new Posn(88,499)).move()
for example, should produce. With a y coordinate of 499, the UFO is close
to the ground. Another plain move would put it under the ground, which
is impossible. Consequently, move should produce a UFO that is on the
ground:
check new UFO(new Posn(88,499)).move()
expect new UFO(new Posn(88,500))
Of course, if move is invoked on a UFO that has landed, nothing happens:
check new UFO(new Posn(88,500)).move()
expect new UFO(new Posn(88,500))
// the world of
// UFOs, AUPs, and Shots
class UFOWorld {
UFO ufo;
AUP aup;
IShots shots;
IColor BACKG = . . . ;
int HEIGHT = 500;
int WIDTH = 200;
...
// move the objects in this world
UFOWorld move() {
return
new UFOWorld(this.ufo.move( this ),
this.aup,
this.shots.move());
}
}
Designing Methods
187
inside of UFO :
// to move this UFO
UFO move() {
. . . this.location . . . this.colorUFO . . .
}
The template points out that an instance of UFO contains two fields: one
records the current location, which move must change, and the other one
records the UFOs color, which is irrelevant to move. It currently does not
have any data, however, that helps decide whether the UFO is close to the
ground or whether it is on the ground.
To overcome this problem, we can choose one of these solutions:
1. equip the UFO class with a field that represents how far down it can
fly in the world. This also requires a modification of the constructor.
2. add a parameter to the move method that represents the height of the
world. Adapting this solution requires a small change to the move
method in UFOWorld, i.e., the addition of an argument to the invocation of UFOs move method.
3. Alternatively, move in UFOWorld can also hand over the entire world
to move in UFO, which can then extract the information that it needs.
We choose the third solution. For an analysis of the alternatives, see the
exercises below.
With the change, we get this revised template:
inside of UFO :
// to move this UFO
UFO move(UFOWorld w) {
. . . w . . . this.location . . . this.colorUFO . . .
}
If you wanted to, you could now add the numeric case distinction that
the study of examples has suggested; check how to develop conditional
methods in section 10.4.
The complete definitions for UFOWorld and UFO are displayed in figure 62. The move method in UFOWorld invokes move from UFO with this:
see the gray-shaded occurrence of this. It is the first time that we use this
as an argument but it shouldnt surprise you. We have used this as a return
value many times, and we always said that this stands for the current object, and objects are arguments that come with method calls. It is perfectly
natural to hand it over to some other method, if needed.
Section 16
188
The move method in UFO first checks whether the UFO has already
landed and then whether it is close enough to the ground. Since these two
computations are really separate tasks, we add them to our wish list:
inside of UFO :
// has this UFO landed yet?
boolean landed(UFOWorld w) { . . . }
// is this UFO about to land?
boolean closeToGround(UFOWorld w) { . . . }
The two are relatively simple methods, and the examples for move readily
induce examples for these two methods.
Exercises
Exercise 16.5 Design the method landed for UFO.
Exercise 16.6 Design the method closeToGround for UFO.
Exercise 16.7 Revise the examples for move in UFO and test the method,
after you have finished exercises 16.5 and 16.6.
Exercise 16.8 You have decided to double the height of the world, i.e., the
height of the canvas. What do you have to change in the move methods?
You are contemplating whether players should be allowed to resize the
canvas during the game. Resizing should imply that the UFO has to descend further before it lands. Does our design accommodate this modification?
Change the move methods in UFOWorld and UFO so that the former
hands the latter the height of this world. Does your answer to the question
stay the same?
Exercise 16.9 Change the UFO class so that it includes an int field that represents the height of the world, i.e., how far down it has to fly before it can
land. Remove the UFOWorld parameter from its move method. Hint: The
changes induces changes to UFOWorld, too.
Once you have completed the design (i.e, run the tests), contemplate
the question posed in exercise 16.8.
In response to this question, you may wish to contemplate whether the
UFO class should include a field that represents the entire UFOWorld in
which it exists. What problem does this design choice pose?
Designing Methods
189
Exercise 16.10 Players find your UFO too predictable. They request that
it should randomly swerve left and right, though never leave the canvas.
Would the current move method easily accommodate this change request?
Note: We have not provided enough information yet for you to design
such a move method. For the impatient, see exercise 19.18.
Designing the move method for the collection of shots is much more
straightforward than move for UFO. Recall the method signature in IShots:
inside of IShots :
// move this list of shots
IShots move();
It says that the method consumes a collection of shots and produces one.
The implementing classes contain concrete method definitions:
inside of MtShots :
IShots move() {
return this;
}
inside of ConsShots :
IShots move() {
return
new ConsShots(this.first.move(),this.rest.move());
}
The one in MtShots just returns the empty list; the other one moves the
first shot and the rest via the appropriate move methods. Since first is an
instance of Shot, this.first.move() uses the yet-to-be-defined method move in
Shot; similarly, since rest is in IShots, this.rest.move() invokes move on a list
of shots.
The move method in Shot is suggested by the design recipe, specifically
by following the containment link. Consider the following examples:
check new Shot(new Posn(88,17)).move()
expect new Shot(new Posn(88,14))
check new Shot(new Posn(88,14)).move()
expect new Shot(new Posn(88,11))
check new Shot(new Posn(88,11)).move()
expect new Shot(new Posn(88,8))
It expresses that an instance of Shot moves at a constant speed of 3 pixels
upwards. Lets assume that it doesnt matter whether a shot is visible on
the canvas or not. That is, the height of a shot can be negative for now.
Then the method definition is straightforward:
Section 16
190
inside of Shot :
// lift this shot by 3 pixels
Shot move() {
return new Shot(new Posn(this.location.x,this.location.y 3));
}
In particular, the definition doesnt create any wishes and doesnt require
any auxiliary methods. You can turn the examples into tests and run them
immediately.
Exercise
Exercise 16.11 Modify the move methods in IShots and implementing classes so that if a shot has a negative y coordinate, the resulting list doesnt
contain this shot anymore. Write up a brief discussion of your design alternatives.
It is finally time for the addition of some action to our game. Specifically, lets add methods that deal with the firing of a shot. As always,
we start with the UFOWorld class, which represents the entire world, and
check whether we need to add an appropriate method there. The purpose
of UFOWorld is to keep track of all objects, including the list of shots that
the player has fired. Hence, when the player somehow fires another shot,
this shot must be added to the list of shots in UFOWorld.
The purpose statement and method signature for shoot in figure 60 express just this reasoning. Here is the template for the method:
inside of UFOWorld :
UFOWorld shoot() {
. . . this.BACKG . . . this.HEIGHT . . . this.WIDTH . . .
. . . this.ufo.shoot(. . . ) . . .
. . . this.aup.shoot(. . . ) . . .
. . . this.shots.shoot(. . . ) . . .
}
It is of course just a copy of the template for draw with new names for the
methods on contained objects. But clearly, BACKG, HEIGHT, WIDTH, and
ufo dont play a role in the design of shoot. Furthermore, while the shots are
involved, they dont shoot; it is the aup that fires the shot. All this, plus the
problem analysis, suggests this code fragment:
Designing Methods
191
inside of UFOWorld :
UFOWorld shoot() {
return
new UFOWorld(this.ufo,
this.aup,
new ConsShots(this.aup.fireShot(. . . ),this.shots));
}
In other words, firing a shot creates a new UFOWorld with the same ufo and
the same aup but a list of shots with one additional shot. This additional
shot is fired from the aup, so we delegate the task of creating the shot to an
appropriately named method in AUP.
The screen shot on page 56 suggests that the AUP fires shots from the
center of its platform. That is, the shot visually originates from the stubby,
vertical line on top of the moving platform. This implies the following
refinement of the purpose statement and method signature:
inside of AUP :
// create a shot at the middle of this platform
Shot fireShot(. . . ) { . . . }
Thus, with an AUP such as this:
new AUP(30)
in a conventional world, we should expect a shot like this:
new Shot(new Posn(42,480))
or like that:
new Shot(new Posn(42,475))
The 42 says that the left corner of the shot is in the middle of the AUP; the
480 and the 475 say that the top of the shot is somewhere above the AUP.
The true expected answer depends on your boss and the visual appearances that you wish to achieve. For now, lets say the second answer is the
one your manager expects.
The third step is to develop the template for fireShot:
inside of AUP :
Shot fireShot(. . . ) {
. . . this.location . . . this.aupColor . . .
}
Section 16
192
The template contains references to the two fields of AUP: its x coordinate
and its color. While this is enough data to create the x coordinate of the
new shot, it doesnt help us with the computation of the y coordinate. To
do so, we need knowledge about the world. Following our above design
considerations, the method needs a UFOWorld parameter:
inside of AUP :
Shot fireShot(UFOWorld w) {
return new Shot(new Posn(this.location + 12,w.HEIGHT 25));
}
Again, consider naming the constants in this expression for the benefit of
future readers.
With fireShot in place, we can finally develop some examples for shoot
and complete its definition. Here are some of the examples in section 6.2:
AUP a = new AUP(90);
UFO u = new UFO(new Posn(100,5));
Shot s = new Shot(new Posn(112,480));
IShots le = new MtShots();
IShots ls = new ConsShots(s,new MtShots());
UFOWorld w1 = new UFOWorld(u,a,le);
UFOWorld w2 = new UFOWorld(u,a,ls);
In this context, you can create several examples for shoot, including
check w1.shoot()
expect new UFOWorld(u,a,new ConsShots(new Shot(102,475),le))
Create additional functional examples and create a thorough test suite.
Exercises
Exercise 16.12 Design move for AUP. The method consumes a String s. If s
is "left", the method moves the AUP by 3 pixels to the left; if s is "right",
the AUP moves 3 pixels to the right; otherwise, it remains as it is.
Modify the method so that if the AUP were to move out of bounds (of
the current world), it doesnt move. Hint: Add a UFOWorld parameter to
the methods signature.
Exercise 16.13 Design the method hit for IShots. The method consumes a
UFO and produces a boolean. Specifically, it produces true, if any of the
shots on the list have hit the given UFO; it produces false otherwise.
Designing Methods
193
To make things simple,25 we say that a shot hits a UFO if the shots location is close enough to the UFO, meaning the distance between the UFOs
center and the location of the shot is less than 10.0 pixels.
Exercise 16.14 Develop the class Player. The class should contain examples of UFOs, AUPs, Shots, and UFOWorlds. Also add the method play of
no arguments. The method creates an instance of Canvas, uses it to draw instances of UFOWorlds, moves all the objects, fires a shot, and repeats these
actions two or three times. If you have solved exercise 16.12, you may also
want play to move the AUP left and right.
As you can see from this case study, following the design recipe is
highly helpful. Once you have read and analyzed the problem statement,
it provides guidance and hints on how to get things done. The case study
also shows, however, that you need to learn to use the steps of the design
recipe in a flexible manner. No design process is perfect; the design recipe
provides you the most valuable guidance in our experience.
While the design template is almost always useful, making up examples and even the statement of a precise purpose statement and method
signature may have to wait until you have explored some of the auxiliary
methods. For example, the design of move showed how you sometimes
cant know the true signature of a method until you are ready to define the
methods body. The design of shoot showed how you may have to postpone
the development of examples for a method and its testing until an auxiliary
method is defined. Otherwise you may not be able to make up proper examples. Still, even when you decide to reorder the steps or to explore the
design of auxiliary methods before you define a method proper, you must
make sure to make up examples before you design the method body. If you
dont, you run the danger to believe what you seeand that almost never
works properly.
25 The
notion of a shot hitting the UFO is something that you would decide on after
some reflection on the geometric shapes of your objects and consultations with someone
who understand geometry well. Put differently, both Shot and the UFO would come with a
method that converts these objects into geometric shapes, and if the two shapes overlap in
any form, we might say that the shot has hit the UFO.
Intermezzo 2
194
Intermezzo 2: Methods
Intermezzo 1 summarizes the language of class and interface definitions
in ProfessorJs Beginner language. In this intermezzo, we extend this language with methods. This time, though, the language discussion requires
four parts not just three: syntax, type checking, semantics, and errors.
an I NTERFACE D EFINTION is:
interface InterfaceN {
MethodSignature;
...
}
Expr.MethodN (Expr, . . . )
Methods
195
Intermezzo 2
196
Methods
197
Type Checking
The first intermezzo (7.3) deals with type checking in a simplistic manner, matching the simplicity of the language covered in part I. Section 13.2
briefly discusses the role of types as a tool for preventing abuses of methods
and thus potential run-time errors.
A short summary of those sections is that the type checker should help
you writing programs. Specifically, it should have rules that disallow the
addition of true to 55 (true + 55) because this makes no sense and would
only trigger a run-time error. Similarly, if a class C doesnt define method
m, then your program should never contain expressions that invoke m on
an instance of C.
import draw.;
interface ILoc {
int xasix();
int yaxis();
double dist(ILoc l);
}
class Loc implements ILoc {
int x;
int y;
Loc(int x, int y) {
this.x = x;
this.y = y;
}
double dist(ILoc l) {
return . . . . . .
}
...
Intermezzo 2
198
World
World
ILoc
ILoc
ILoc
Loc
Loc
Name
+
||
onTick
bigBang
xaxis
yaxis
dist
new
dist
Domain
int, int
boolean,boolean
ILoc
int, int
ILoc
Range
int
boolean
World
boolean
int
int
double
Loc
double
Methods
199
The line separates those parts of the table that originate from Java and the
library from those that originate from explicit interface and class definitions. Before you proceed, take a close look at the table to understand its
pieces.
Once you have a type context and a signature table, you check each
kind of expression according to how it is constructed:
1. As discussed before, every constant has an obvious type.
2. To determine the type of a primitive expression, first determine the
types of all sub-expressions and the signature of the operator. If the
types of the sub-expressions match the types of the operators domain (in the right order), the expressions type is the range type of
the operator. If not, the type checker signals a type error.
Example 1:
32 + 10
The sub-expressions are 32 and 10 and have the types int and int. The
operators signature is int and int for the domain, which matches the
types of the sub-expressions. Therefore the type of the entire expression is int.
Example 2:
(32 + 10) || true
The sub-expressions are (32 + 10) and true. We know from the first
example that the type of the first sub-expression is int; the type of
the second one is boolean by the first rule. The signature of || is
boolean and boolean for the domain. While the type of the second
sub-expression matches the corresponding type in the operators domain, the first one doesnt. Hence, you have just discovered a type
error.
3. To determine the type of a parameter, look in the type context.
4. The type of this is always the name of the class in which it occurs.
5. To determine the type of a field selection e.fieldName, first determine
the type of e. If it isnt the name of a class, the type checker signals an
error. If it is, say C, and if the type context contains a field of name
fieldName with type T for C, then the type of the entire expression is T.
Intermezzo 2
200
Example 3:
. . . l.x . . .
If this expression occurs in lieu of the in figure 64, then the parameter l has type ILoc according to the type context. Since ILoc is an
interface and not a class, it cant support field access, so what you see
here is an example of a type error concerning field access.
If you change the program just slightly, like this:
Example 4:
double dist(Loc l) {
. . . l.x . . .
}
then everything works out. The parameter l now has type Loc, which
is a class and which contains a field named x. The fields type is int so
that l.x has type int.
6. For a constructor call, the type checker determines the types of all
the arguments. It also determines the signature of the new constructor in the signature table. Now, if the types of the arguments match
the types of the constructors domain (in the proper order), the expressions type is the class of the constructor. If not, the type checker
signals a type error.
7. Finally, to determine the type of a method call ec.methodName(e1,. . . ),
the type checker starts with the expression ec. Its type must be the
name of a class or the name of an interface. If so, the signature can be
extracted from the signature table. If not, the type checker signals an
error.
Second, the type checker determines the types of the arguments. They
must match the types of the methods domain (in the proper order). If
so, the expressions type is the range type from the method signature;
otherwise, there is a type error.
Example 5:
new Loc(10,20).dist(new Loc(30,40))
Methods
201
Here the object expression is new Loc(10,20), which by the rules for a
constructor expression has type Loc. According to the signature table,
this class contains a method called dist with domain ILoc and range
double.
Next, the argument expression is new Loc(30,40), and it has type Loc.
Since Loc implements ILoc, the types match. The type of the example
expression is therefore double.
Several of these clauses use the phrase the types match. What this means,
for now, is that the types are either equal or that the first type is a class type
and implements the second one, which is an interface type.
Type checking statements is like type checking expressions. Again, we
look at statements case for case, using type contexts and signature tables:
1. A return statement consists of a keyword and an expression. This
implies that the type checker must determine the type of the expression. After that, it must match that type and the specified range of
the method that contains the return statement. If the types match, the
statement type checks; otherwise, the type checker reports an error.
Example 6:
. . . return 4.0; ...
In the context figure 64, the type of 4.0 is double, which is also the
range of the signature of dist. Hence, this return statement typechecks.
In contrast, Example 7:
. . . return this.x; ...
this has type Loc and this.x has therefore type int, which is not a match
for double.26
2. An if statement is more complex than a return statement and, yet,
type-checking it is straightforward. The type checker determines the
type of the test expression. If it isnt boolean, the type checker signals
a type error. If it is, the type checker makes sure that the statements
26 In Java, an int is automatically converted into a double in such situations. This automatic conversion simplifies life for a programmers, but it also introduces opportunities for
mistakes for novices. Therefore ProfessorJ does not match int and double.
Intermezzo 2
202
Methods
203
we sketch the evaluation rules and practice them with examples. Together
with your understanding of the evaluation of Scheme programs, this sketch
should suffice for thinking about the Java programs in this book.27
Lets study the collection of expressions case-by-case to see how to evaluate them. Instead of going through them in order, we start with constants
and constructor calls:
1. Constants such as "hello world", 5, 8, true are primitive values.
2. As Intermezzo 1 shows, a constructor call evaluates to an instance of
the specified class if the argument expressions are values; if not, we
must reduce them to values first. We loosely use the word VALUE for
these instances, too; many other texts use value for constants only.
To jog your memory, we distinguish two kinds of constructor calls.
The first concerns a class without initialization equations.
Example 1:
class Dog {
String name;
int age;
Dog(. . . ) { . . . }
}
In this context, the constructor new Dog("woof",2) call determines the
instance completely and immediately. We can use the call in place of
the instance.
The second concerns a class with initialization equations.
Example 2:
class Ball {
int x;
int y;
IColor c = new Red();
Ball(. . . ) { . . . }
}
27 For those readers interested in becoming good programmers, we recommend taking a
college course on programming languages that demonstrates how to implement evaluation
rules.
Intermezzo 2
204
Methods
205
// just a number
int stuff () {
return 10;
}
Intermezzo 2
206
// just a number
int stuff () {
return 10 + 20;
}
Evaluating the method call 3 (new Sample().stuff ()) now requires the
evaluation of a primitive expression inside the return statement:
3 (new Sample().stuff ())
;; reduces to
3 (return 10 + 20)
;; reduces to
3 (return 30)
;; reduces to
3 30
We have surrounded the factors of the multiplication in area with optional parentheses to clarify the rest of the example.
After clicking RUN, you enter new Rectangle(3,5).area() in the interactions window and expect to see 15. Here is how this comes about.
The target expression is a value so we can just look at the method
body and substitute the parameters:
(new Rectangle(3,5). w) (new Rectangle(3,5). h)
Methods
207
The method has only one parameter: this. As prescribed, this has
been replace by the value of the target expression, which we identify
with the constructor call. The next two expression we must evaluate
are field selector expressions.
For the remaining expressions, we can now proceed more rapidly:
5. An expression that is just a parameter name (ParameterN) cant show
up according to our rules. Such a name is just a placeholder for a
value. As soon as we match up a placeholder and its actual value, we
replace the name with the value.
6. The preceding remark applies to this, too, because this is just a special
parameter.
7. This leaves us with expressions for field access. It is similar to a
method call but simpler than that. Like a method call, a field access
expression starts with a target expression. The rest, however, is just
the name of some field:
targetExpression.fieldName
Before you proceed, you must reduce targetExpression to a value. Proceeding from here depends on that value. If it is an instance of a class
in the definitions window, the value has roughly this shape:
new aClass(someValue1, . . . )
where someValue1 and so on are the values that went into the construction of the instance.
To finish the evaluation of the field access expression, we must distinguish between two cases of constructor calls (see intermezzo 7.2):
(a) If the constructor call involves a class that has no initialization
equations, then the constructor call has as many argument values as there are fields in the class. It is thus possible to extract
the desired field value from the list of constructor arguments.
Example 5a:
Intermezzo 2
208
class Rectangle {
int w;
int h;
Rectangle(int w, int h) {
this.w = w;
this.h = h;
}
}
The expression new Rectangle(2,5).h uses a constructor call as the
target expression. Furthermore, the class of the constructor call
uses has no initialization equations. Hence, the second argument of the call corresponds to the second field of the class, and
therefore, the result of the field access expression is 5.
Given this much, we can also finish the evaluation of example 4.
We had stopped at the expression
(new Rectangle(3,5). w) (new Rectangle(3,5). h)
because it required two field accesses (in the same context as
example 5a). Now we know how to continue:
(new Rectangle(3,5). w) (new Rectangle(3,5). h)
;; reduces to
3 (new Rectangle(3,5). h)
;; reduces to
35
;; reduces to
15
(b) If the class has initialization equations for fields, you need to
look at the entire created target object, i.e., the value of the target expression, to extract the value of the field. This can take
two different forms. On one hand, you can have an initialization
equation directly with a field:
Example 5b:
Methods
209
class Rectangle {
int w;
int h;
IColor displayColor = new Blue();
Rectangle(int w, int h) {
this.w = w;
this.h = h;
}
}
Here the class comes with three fields, one of which is initialized
immediately and independently of the constructor (and its parameters). If a field access expression uses displayColor, you can
read it off from the definition of the class.
On the other hand, we have seen that a class may have a field
that is initialized within the constructor via a computation. The
first concrete example came up on page 14.2 when we created
a canvas whose size depended on the parameters of the class
constructor. Another small variation on example 5a illustrates
the point just as well:
Example 5c:
class Rectangle {
int w;
int h;
int area;
Rectangle(int w, int h) {
this.w = w;
this.h = h;
this.area = w h;
}
}
If you evaluate the expression new Rectangle(2,5), you now get
a class with three fields, even though your constructor call has
just two arguments. To extract the area field, you have no choice
but to look at the entire target object:
Rectangle(x = 2, y = 5, area = 10)
The field area is initialized to 10, which is thus the result of the
field access expression.
Intermezzo 2
210
Methods
211
Since this last return statement contains a plain String value, the value
of the statement is "large".
Exercises
Exercise 17.1 Take a look at this program:
class Room {
Loc l;
Room(Loc l) { this.l = l; }
// how far away is this room from (0,0)?
double distance() { return this.l.distance(); }
}
class Loc {
double x;
double y;
Loc(double x, double y) {
this.x = x;
this.y = y;
}
// how far away is this location from (0,0)?
double distance() {
return Math.sqrt((this.x this.x) + (this.y this.y));
}
}
Explain how ProfessorJ determines that new Room(new Loc(3.0,4.0)) has
the value 5.0. Use the numbers of the rules above for your explanation of
each step.
Exercise 17.2 The interface and class definitions in figure 65 sketch a phone
setup where a landline may enable forwarding to a cellphone. (In some
countries this mechanism reduces the cost for calls to a cellphone number.)
Explain how ProfessorJ determines that the evaluation of the expression
new LandLine(true,true,new CellPhone(true)).dial()
produces true. Use the numbers of the rules above for your explanation of
each step.
Intermezzo 2
212
boolean dial() {
boolean dial() {
return !(this.busy);
if (this.fwd) {
return this.fwdNumber.dial(); }
else {
return !(this.busy); }
}
Methods
213
class Loc {
int x;
int y;
Room(int x, int y) { . . . }
// is this close to ?
int volume(int x, int x) {
return . . . ;
This class contains a method whose parameter list uses the same parameter
name twice. This is reminiscent of the syntax errors we discussed in the
first intermezzo. Run this example in ProfessorJ and carefully read its error
message.
Next take a look at this grammatically correct class:
class Room {
int x;
int y;
int area;
IColor c;
Room(int x, int y, AClass c) {
this.x = x;
this.y = y;
this.area = x c;
this.c = x;
}
}
The creator of this class messed up the initialization equation for the area
field. The right hand side is an expression that multiplies an int (x) with
a color (c). While this expression is properly formed from two parameter
names and a operator, it violates the signature of .
Similarly, the following two-class program is also grammatically correct
but contains a type error:
Intermezzo 2
214
class Room {
int w;
int h;
Room(int w, int h) {
this.w = w;
this.h = h;
}
int area() { return this.w this.h; }
}
class Examples {
Room r = new Room();
boolean test1 = check this.r.erah() expect 10;
Examples() { }
}
The problem is that the check expression in Examples invokes a method
erah on the target expression this.r, which has type Room. This latter class,
however, does not support a method named erah and, therefore, the type
checker signals an error.
interface IList {
// find the ith element
// in this list
String find(int i);
}
String find(int i) {
if (i == 0) {
return this.first; }
else {
return this.rest.find(i-1); }
}
Methods
215
for two. First, your program may ask the computer to divide a number
by 0. In general, there are some primitive operations that are not defined
on all of their inputs. Division is one example. Can you think of another
one from high school mathematics? Second, your program may contain a
method that signals an error for certain inputs. Recall the Dot class from
page 12.2 with its area method:
class Dot {
Posn p;
Dot(Posn p) { this.p = p; }
double area() {
}
Its area method raises an error for all inputs. You may also encounter PAR TIAL methods, which are like division in that they work for some, but not
all inputs: see figure 66. There you see a data representation of lists of
Strings, with a function for extracting the ith element. Naturally, if i is too
large, the method cant return such an element.
Finally, in addition to those programming mistakes that ProfessorJ can
discover for you, there are also logical mistakes. Those are programming
errors that dont violate the grammar, the typing rules, or the run-time conditions. They are computations that produce incorrect values, i.e., values
that dont meet your original expectations. In this case, youre facing a
LOGICAL ERROR . Unless you always develop tests for all possible methods
and careful inspect the results of test runs, you can easily overlook those
logical errors, and they may persist for a while. Even if you do test rigorously, keep in mind that the purpose of tests is to discover mistakes. They
cant prove that you didnt make any.
Exercises
Intermezzo 2
216
class Jet {
String direction;
int x;
Jet(. . . ) { . . . }
String control(String delta, int delta) {
...
}
}
Is it grammatically correct (as far as shown)? Does it violate any typing
rules?
Exercise 17.2 The following class definition violates the typing rules:
class Ball {
double RADIUS = 4.0;
int DIAMETER = this.RADIUS;
String w;
Ball(int w) {
this.w = w;
}
}
Explain why, using the number of the typing rules in the subsection on type
checking in this intermezzo.
Exercise 17.3 The following class definition violates the typing rules:
interface IWeapon {
boolean launch();
}
class Rocket implements IWeapon {
...
boolean launch(int countDown) { . . . }
}
Determine whether this sketch is grammatically correct. If so, check does it
satisfy the tying rules?
Methods
TODO
Should we ask students to represent all of Beginners syntax?
Should we ask students to write a type checker?
217
PICTURE:
Methods
219
220
Intermezzo 2
TODO
introduce more Example stuff, plus abstracting over tests (?)
use Java 1.5 overriding: its okay to decrease the type in the return
position (all covariant positions now): explain and use
say: public is needed to implement an interface method
do we need a section that shows how to abstract within one class?
perhaps in Part II already?
abstraction in TESTS Examples classes
III
Many of our classes look alike. For example, the UFO class has strong
similarities to the Shot class. Both have similar purpose statements, have
the same kinds of fields, and have methods that simulate a move along the
vertical axis.
As How to Design Programs already says, repetitions of code are major sources of programming problems. They typically come about when
programmers copy code. If the original code contains an error, the copy
contains it, too. If the original code requires some enhancement, it is often necessary to modify the copies in a similar manner. The programmers
who work on code, however, are often not the programmers who copied
the code. Thus they are often unaware of the copies. Hence, eliminating
errors becomes a cumbersome chase for all the copies and, therefore, an
unnecessarily costly process.
For these reasons, programmers should eliminate similarities whenever
possible. In How to Design Programs, we learned how to abstract over similarities in functions and data definitions and how to reuse existing abstractions. More generally, we took away the lesson that the first draft of a program is almost never a finished product. A good programmer reorganizes
a program several times to eliminate code duplications just as a good writer
edits an essay many times and a good painter revises an oil painting many
times. Good pieces of art are (almost) never created in a single session; this
includes programs.
Class-based, object-oriented languages such as Java provide a number
of abstraction mechanisms. In this chapter we focus on creating superclasses for similar classes and the derivation of subclasses from existing
classes. The former is the process of abstraction; its result is an abstraction.
The later is the use of an abstraction; the result is a well-designed program.
ProfessorJ:
Intermediate
Section 18
222
18 Similarities in Classes
Similarities among classes are common in unions. Several variants often
contain identical field definitions. Beyond fields, variants also sometimes
share identical or similar method definitions. In the first few sections of this
chapter, we introduce the mechanism for eliminating these similarities.
+--------+
| IShape |
+--------+
+--------+
|
/ \
--|
|
+-------------+
+--------+
| Shape
| +->| CartPt |
+-------------+ | |--------+
| CartPt loc |--+ | int x |
+-------------+
| int y |
|
+--------+
/ \
--|
+---------+--------------+
|
|
|
|
|
|
+-----+ +--------+ +--------+
| Dot | | Square | | Circle |
+-----+ +--------+ +--------+
|
| | int s | | int r |
+-----+ +--------+ +--------+
Similarities in Classes
223
// a square shape
class Square
extends Shape {
// a circle shape
class Circle
extends Shape {
Section 18
224
// geometric shapes
interface IShape {}
}
// a dot shape
class Dot
extends Shape {
Dot(CartPt loc) {
super(loc);
}
}
// a square shape
class Square
extends Shape {
int size;
Shape(CartPt loc) {
this.loc = loc;
}
// a circle shape
class Circle
extends Shape {
int radius;
The introduction of a superclass for geometric shapes raises the question what Square, for example, really looks like. We know that Shape contains one field and that extends means inherit all the fields from the superclass. Hence Square contains two fields: loc of type CartPt and size of type
int. The Square class does not inherit Shapes constructor, which leads to the
question what the constructor of Square looks like. There are actually two
answers:
Square(CartPt loc, int size) {
this.loc = loc;
this.size = size;
}
The left one initializes both fields at once, ignoring that Squares declaration
of the loc field is actually located to its superclass. The right one contains
a novel construct: super(loc), known as a SUPER CONSTRUCTOR. That is,
the constructor doesnt just consist of equations, but the expression super(loc). This expression invokes the constructor of the superclass, handing
over one value for the initialization of the superclasss loc field.
Now take a look at this expression:
Similarities in Classes
225
Exercises
Exercise 18.1 Add constructors to the following six classes:
1. train schedule:
class Train {
Schedule s;
Route r;
}
2. restaurant guides:
Section 18
226
class Restaurant {
String name;
String price;
Place place;
}
3. vehicle management:
class Vehicle {
int mileage;
int price;
}
Similarities in Classes
227
interface IShape {
// to compute the area
// of this shape
double area();
// to compute the distance of
// this shape to the origin
double distTo0();
}
class Dot
extends AShape {
class Square
extends AShape {
int size;
class Circle
extends AShape {
int radius;
// cons. omitted
...
double area() {
return
this.size
this.size;
}
// cons. omitted
...
double area() {
return
Math.PI
this.radius
this.radius;
}
// cons. omitted
...
double area() {
return .0;
}
double distTo0() {
return this.loc.distTo0();
}
...
double distTo0() {
double distTo0() {
return this.loc.distTo0();
}
...
return
this.loc.distTo0()
this.radius;
}
...
}
Figure 69: Classes for geometric shapes with methods and templates
Section 18
228
Similarities in Classes
inside of Car :
229
inside of Truck :
inside of Bus :
The union has three variants: for cars, trucks, and buses. Since each variant
needs to describe the tank size of the vehicle, there is a common, abstract
superclass with this field. Each variant separately comes with a method
for calculating the cost of filling the tank.29 The cost methods have been
designed systematically and are therefore identical.
When the exact same method definition shows up in all the variants of
a union with a common superclass, you can replace the abstract method in
the superclass with the method definition from the variants:
abstract class AVehicle {
double tank;
...
double cost(double cp) {
return this.tank cp;
}
}
Furthermore, you can delete the methods from Car, Truck, and Bus because
the lifted cost method in AVehicle is now available in all three subclasses.
Naturally, after editing a class hierarchy in such an intrusive manner,
we must re-run the test suite just to make sure we didnt accidentally introduce a mistake. Since the test suites for AVehicle construct instances of
Car, Truck, and Bus and then use cost, and since all three classes inherit this
method, the test suite should run without error as before.
Exercise
Exercise 18.5 Develop examples for the original class hierarchy representing vehicles. Turn them into tests and run them. Then lift the common cost
method and re-run the tests.
Section 18
230
interface IShape {
// compute the area of this shape
double area();
// is this shape larger than that shape
boolean larger(IShape that);
}
abstract class AShape implements IShape {
CartPt loc;
...
abstract double area();
abstract boolean larger();
}
Here the interface specifies just two methods: larger and area. The former
determines whether this shape is larger than some other shape, and the
latter actually computes the area of a shape. Here are the definitions of area
and larger:
inside of Dot :
boolean larger(IShape
return
this.area()
>
that.area();
}
double area() {
return 0;
that) {
inside of Square :
int size;
...
boolean larger(IShape
that) {
return
this.area()
>
that.area();
}
inside of Circle :
int radius;
...
boolean larger(IShape
that) {
return
this.area()
>
that.area();
}
double area() {
double area() {
return
this.size
this.size;
}
}
return
Math.PI
this.radius
this.radius;
We have seen area before and there is nothing new to it. The larger method
naturally takes advantage of the area method. It computes the area of this
shape and the other shape and then compares the results. Because this
follows the one task, one function rule of design, all three definitions of
larger are identical.
Similarities in Classes
231
Figure 70 shows the result of lifting larger to AShape. Now the abstract
superclass not only contains a shared CartPt field but also a shared method.
Interestingly the latter refers to another, abstract method in the same class
but this is acceptable because all shapes, abstract or concrete, implement
IShape, and IShape demands that shapes define an area method.
interface IShape {
// to compute the area of this shape
double area();
class Dot
extends AShape {
class Square
extends AShape {
int size;
class Circle
extends AShape {
int radius;
// cons. omitted
...
double area() {
return
this.size
this.size;
}
...
// cons. omitted
...
double area() {
return
Math.PI
this.radius
this.radius;
}
...
// cons. omitted
...
double area() {
return .0;
}
...
}
}
Section 18
232
because the closest point to the origin is on the perimeter of the circle.30
interface IShape {
// to compute the distance of
// this shape to the origin
double distTo0();
...
}
return this.loc.distTo0();
}
...
}
class Dot
extends AShape {
class Square
extends AShape {
int size;
class Circle
extends AShape {
int radius;
// cons. omitted
...
// cons. omitted
// cons. omitted
...
double distTo0() {
return
this.loc.distTo0()
this.radius;
}
...
}
Figure 71: Classes for geometric shapes with methods and templates
In object-oriented programming languages the common method may
be defined in the abstract class. As before, every subclass inherits this definition. For those subclasses for which this method definition is wrong, you
(the programmer) override it simply by defining a method with the same31
method signature. Figure 71 shows the result of transforming the classes in
figure 69 in this manner. The class AShape defines distTo0 as the distance of
loc to the origin. While Dot and Square inherit this method, Circle overrides
it with the one that makes sense for circles.
Of course, in some sense the distance to the origin of all shapes depends
on the distance to the origin of their anchor point. It just so happens that
the distance between the origin and the anchor point of, say, a Square is the
30 Remember
our assumption that the shapes are completely visible on the canvas.
Java, you can use a signature whose return type is a subtype of the overridden
method; in other object-oriented languages, the signatures have to be identical.
31 In
Similarities in Classes
233
interface IShape {
// to compute the distance of
// this shape to the origin
double distTo0();
...
}
return this.loc.distTo0();
}
...
}
class Dot
extends AShape {
class Square
extends AShape {
int size;
class Circle
extends AShape {
int radius;
// cons. omitted
...
// cons. omitted
// cons. omitted
...
double distTo0() {
return
super.distTo()
this.radius;
}
...
}
Figure 72: Classes for geometric shapes with methods and templates
distance between the shape and the origin whereas the distance between
the origin and the center of a Circle is not. In a way, this is just what inheritance is about. The child is somewhat like the parents, but not the same.
As it turns out, this idea is important in programming too, and you can express this subtle but important relationship between children and parents
in object-oriented programming languages.
Figure 72 shows how to express this like-but-not-the-same relationship
in classes. The gray-shaded expression is a SUPER METHOD call. The super
refers to the superclass of Circle, which is AShape. The expression invokes
the distTo0 method in AShape (on this circle) and thus computes the distance
of the anchor point of the circle to the origin; the rest of the expression
subtracts the length of the radius, as before.
Because of the simplicity of distTo0, expressing the relationship as carefully as we have done here appears to be pedantic. These appearances are
deceiving, however. Programming is about subtleties and expressing them
Section 18
234
is important. It is natural in our profession that other people will read your
program later and modify it. The clearer you can communicate to them
what your program is about, the higher the chances are that they dont
mess up these carefully thought-through relationships.
Exercise
Exercise 18.6 Complete the class hierarchy for overlapping shapes from
section 15.3: IComposite, Square, Circle, and SuperImp with all the methods:
area, distTo0, in, bb. Add the following methods to the class hierarchy:
1. same, which determines whether this shape is of equal size as some
other, given IShape up to some given small number delta;
2. closerTo, which determines whether this shape is closer to the origin
than some other, given IShape;
3. drawBoundary, which draws the bounding box around this shape.
Develop examples and tests for as many methods as possible. Then introduce a common superclass for the concrete classes and try to lift as many
fields and methods as possible into this superclass. Make sure that the revised classes function properly after this editing step.
Similarities in Classes
235
// recording temperature
// measurements [in F]
class Temperature {
int high;
int today;
int low;
Temperature(int high,int today,int low) {
this.high = high;
this.today = today;
this.low = low;
}
int dHigh() {
int dHigh() {
int dLow() {
int dLow() {
String asString() {
return String.valueOf (high)
.concat("-")
.concat(String.valueOf (low))
.concat("hPa");
}
String asString() {
return String.valueOf (high)
.concat("-")
.concat(String.valueOf (low))
.concat("F");
}
Section 18
236
class Recording {
int high;
int today;
int low;
Recording(int high,int today,int low) {
this.high = high;
this.today = today;
this.low = low;
}
int dHigh() {
int dLow() {
String asString() {
return
String.valueOf (high).concat("-").concat(String.valueOf (low));
}
String asString() {
return super.asString()
.concat("mm");
}
String asString() {
return super.asString()
.concat("F");
}
The first solution is to mimic what we have seen in the previous section.
It is shown in figure 74, which displays the new common superclass and the
revised versions of the original classes. The latter two extend Recording and
thus inherit its three fields (high, today, and low) as well as its three methods
(dHigh, dLow, and asString). The asString method produces a string that
Similarities in Classes
237
class Recording {
int high;
int today;
int low;
String unit;
Recording(int high,int today,int low,String unit) {
this.high = high;
this.today = today;
this.low = low;
this.unit = unit;
}
int dHigh() { . . . }
int dLow() { . . . }
String asString() {
return
String.valueOf (high).concat("-").concat(String.valueOf (low)).concat(this.unit);
}
separates high and low with a dash (). Each subclass overrides the asString
method to compute a representation that also includes the physical units.
Note how the two subclasses use a super constructor call to initialize the
objects. This creates an instance of Pressure or Temperature but initializes the
fields they inherit from Recording.
The use of the super constructor also suggests an alternative solution,
which is displayed in figure 75. For this second solution, Recording contains
a complete definition of asString and the additional, String-typed field unit.
The asString method uses this new field as the name of the physical unit.
The constructors in the subclasses have the same signature as those in the
original class but supply an appropriate unit name as the fourth argument
238
Section 18
really just need an abstract field not method to hold the unit but Java and most
object-oriented languages dont support this mechanism.
34 The idea of collecting programming patterns emerged in the 1990s. Gamma, Helm,
Johnson, and Vlissides conducted software archaeology, uncovered recurring patterns
of programming, and presented them in a stylized manner so that others could benefit
from them. Our design recipes emerged during the same time but are based on theoretical
Similarities in Classes
239
String asString() {
return
String.valueOf (high).concat("-").concat(String.valueOf (low)).concat(this.unit());
}
abstract String unit();
}
class Temperature extends ARecording {
Temperature(int high,int today,int low) {
super(high,today,low);
}
String unit() {
return "F";
}
String unit() {
return "hPa";
}
in the superclass is a template that lays out what the string basically looks
like, up to a hole; the unit method is a hook that empowers and forces subclasses to fill the hole.
The template-and-hook pattern can occur more than once in a class hiconsiderations mixed with our Scheme programming experience. The critical point is that
following the design recipe often produces code just like the one that software patterns
suggest in the same situation.
Section 18
240
+---------------+
| ARecording
|
+---------------+
| int high
|
| int today
|
| int low
|
+---------------+
| int dHigh()
|
| int dLow()
|
| String
|
| asString()
|
| String unit() |
+---------------+
|
//
/ \
//
--//
|
//
---------------------------//----------|
|
//
|
+-------------+ +---------------+ // +---------------+
| Pressure
| | ATemperature | // | Precipitation |
+-------------+ +---------------+ // +---------------+
+-------------+ +---------------+ // +---------------+
|String unit()| | String unit() | // | String unit() |
+-------------+ | String name() | // +---------------+
+---------------+ //
|
//
/ \
//
--//
|
--------------------|
|
+-------------+
+---------------+
| Celsius
|
| Fahrenheit
|
+-------------+
+---------------+
+-------------+
+---------------+
|String name()|
| String name() |
+-------------+
+---------------+
erarchy, and a class hierarchy may contain more than one level of classes.
Suppose your manager wishes to supply temperature measurements in
both Fahrenheit and Celsius degrees. In that case, a simple Temperature
class is inappropriate. Instead, you should introduce two classes: one for
Celsius and one for Fahrenheit measurements. At the same time, the two
classes share a lot of things. If you were requested to add a temperaturespecific method, they would probably share it, too. Put differently, as far as
we can tell now, they share everything except for the name of the physical
unit. Thus, the best course of action is to turn Temperature into an abstract
class and extend it with a subclass Celsius and a subclass Fahrenheit.
Now take a look at the class diagram in figure 77. The diagram has three
levels of classes, the second level extending the first, and the third extending the second. Furthermore, the extension of ARecording to ATemperature
is a template-and-hook pattern, and so is the extension of ATemperature to
Similarities in Classes
241
Celsius or Fahrenheit.
Lets inspect the actual code. Here is the new abstract class:
abstract class ATemperature extends ARecording {
ATemperature(int high,int today,int low) {
super(high,today,low);
}
String unit() {
return " degrees ".concat(this.name());
}
abstract String name();
}
The class defines unit but only as a template, with a hole of its own. The
hook for unit is name, a method that names the degrees in the string representation. As before, making name abstract means that the entire class is
abstract and that a subclass must define the method, if it is to be concrete.
The Celsius and Fahrenheit classes are concrete:
class Celsius
class Fahrenheit
extends ATemperature {
extends ATemperature {
Celsius(int high, int today,int low) { Fahrenheit(int high,int today,int low) {
super(high,today,low);
super(high,today,low);
}
}
String name() {
return "Celsius";
}
String name() {
return "Fahrenheit";
}
The two name methods in the two subclasses just return "Celsius" and
"Fahrenheit", respectively. Indeed, the two are so alike that they call for
more abstraction, except that most programming languages dont have the
mechanisms for abstracting over these similarities.35
Once you have a properly organized program, modifications and extensions are often easy to add. Figure 77 indicates one possible extension: a
35 Physical
and monetary units are ubiquitous fixtures in data representations. In principle, a programming language should provide mechanisms for annotating values with such
units, but so far no mainstream programming language does. The problem of adding units
to languages is complex; programming language researchers have been working on this
problem for two decades.
Section 18
242
class for measuring Precipitation. Measuring the amount of rain and snow
is a favorite and basic topic of meteorology. Also, people love to know how
much it rained on any given day, or when they travel somewhere, how
much rain they may expect according to historical experience. There is a
difference between precipitation and temperature, however. While the former has a natural lower boundnamely, 0the latter does not, at least as
far as people are concerned.36
Within the given framework of classes, creating Precipitation measurements is now just a matter of extending ARecording with a new subclass:
// recording precipitation measurements [in mm]
class Precipitation extends ARecording {
Precipitation(int high,int today) {
super(high,today,0);
}
// override asString to report a maximum value
String asString() {
return "up to ".concat(String.valueOf (high)).concat(this.unit());
}
// required method
String unit() {
return "mm";
}
This supplies every field and method that comes with a recording. Still,
because of the special nature of precipitation, the constructor differs from
the usual one. It consumes only two values and supplies the third one to
the super constructor as a constant. In addition, it overrides the asString
method completely (without super), because an interval format is inappropriate for reporting precipitation.
Here is another possible change request:
. . . The string representation of intervals should be as mathematical as possible: the low end followed by a dash and then
the high end of the interval. . . .
0o K is the absolute zero point for measuring temperatures, but we can
safely ignore this idea as far as meteorology information for people is concerned.
36 Technically,
Similarities in Classes
243
All subclassesPressure, ATemperature, Precipitation, Celsius, and Fahrenheitinherit the modified computation or override it appropriately.
The lesson of this exercise is again that creating a single point of control
for a single piece of functionality pays off when it is time to change the
program.
Exercises
Exercise 18.7 Figure 77 introduces a union from two classes that had been
developed independently. Create an interface for the union.
Exercise 18.8 Add a class for measuring precipitation to the class hierarchy
of recordings (version 1) in figure 74 . Also add a class for measuring precipitation to the second version of the class hierarchy (figure 75). Compare
the two hierarchy extensions and the work that went into it.
Exercise 18.9 Add a class for measuring the speed of the wind and its direction to all three class hierarchies in figures 74 , 75, and 76.
Exercise 18.10 Design newLow and newHigh. The two methods determine
whether todays measurement is a new historical low or high, respectively.
Exercise 18.11 Design the methods fahrenheitToCelsius and celsiusToFahrenheit for Fahrenheit and Celsius in figure 77, respectively.
Exercise 18.12 A weather station updates weather recordings on a continuous basis. If the temperature crosses a particular threshold in the summer,
a weather station issues a warning. Add the method heatWarning to the
class hierarchy in figure 77. The method produces true if todays temperature exceeds a threshold, false otherwise. For Fahrenheit measurements,
the threshold is 95o F, which corresponds to 35o C.
Section 18
244
Exercise 18.13 If the air pressure falls below a certain threshold, a meteorologist speaks of a low, and if it is above a certaing level, the weather
map display high. Add the methods lowPressure and highPressure to the
class hierarchy in figure 77. Choose your favorite thresholds.
Abstracting is a subtle and difficult process. It requires discipline and
experience, acquired via practice. Lets therefore look at a second example,
specifically the star thaler problem from chapter II:
. . . Develop a game based on the Grimms brothers fairy tale
called Star Thaler. . . . Your first task is to simulate the movement of the falling stars. Experiment with a single star that is
falling to the ground at a fixed number of pixels per time unit
on a 100 100 canvas. Once the star has landed on the ground,
it doesnt move anymore. . . .
After some experimentation your programming team finds that the game
may sell better if something unforeseen can happen:
. . . Modify the game so that it rains red rocks in addition to
golden star thalers. If the girl is hit by one of the rocks, her
energy decreases. Assume that a red rock has the same shape
as a star but is red. . . .
It is your task to modify your program so that your managers can visualize
the game with thalers and red rocks.
Figure 78 shows the result in two columns. The left column is the class
of star thalers, as partially developed in chapter II; the right column contains the class of red rocks. The classes have been arranged so that you can
see the similarities and the differences. Here are the obvious similarities:
1. Both classes have x and y of type int.
2. Both classes have identical landed methods.
3. Both classes have identical nearGround methods.
And there are also two less obvious connections between the classes:
4. The Star class contains a DELTA field for specifying how far a star
thaler drops at each step; the analogous field in RedRock is deltaY.
If we systematically rename DELTA to deltaY in Star (see gray highlighting), the two fields are also an obvious overlap between the two
classes.
Similarities in Classes
245
class RedRock {
int x;
int y;
int deltaX = 3;
int deltaY = 5;
class Star {
int x = 20;
int y;
int DELTA = 5;
RedRock(int x,int y) {
this.x = x;
this.y = y;
}
boolean draw(Canvas canv) {
. . . new Red()) . . . }
RedRock drop() {
if (this.landed()) {
return this; }
else { if (this.nearGround()) {
return
new RedRock(this.x,100); }
else {
return
new RedRock(this.x + this.deltaX,
this.y + this.deltaY); }
}
}
boolean landed() {
return this.y == 100;
}
boolean nearGround() {
return this.y + this.deltaY > 100;
}
Star(int y) {
this.y = y;
}
boolean draw(Canvas canv) {
. . . new Yellow() . . . }
Star drop() {
if (this.landed()) {
return this; }
else { if (this.nearGround()) {
return
new Star(100); }
else {
return
new
Star(this.y + this.DELTA); }
}
}
boolean landed() {
return this.y == 100;
}
boolean nearGround() {
return this.y + this.DELTA > 100;
}
boolean hit(Girl g) {
return
g.closeTo(new Posn(this.x,this.y));
5. Both classes also contain similar but not identical draw methods. One
uses yellow as the drawing color, the other one uses red.
What distinguishes the two classes are the drop methods. While a star thaler
Section 18
246
goes straight down, a rock drops in a diagonal line. For every five pixels
that a red rock drops, it moves three to the right. Finally, RedRock contains
one method more than Star: hit. It determines whether the rock has hit the
girl.
Exercises
Exercise 18.14 The code in figure 78 comes without any purpose statements and without tests. Write down concise purpose statements for all
methods, and develop a test suite for both classes. Use this definition for
Girl:
class Girl {
Girl() { }
// is this girl close to Posn p?
boolean closeTo(Posn p) {
return false;
}
}
This is called a stub implementation.
Exercise 18.15 The method definitions for draw in figure 78 contain new
expressions for colors. Introduce a name for each of these colors. Dont
forget to run the test suites from exercise 18.14.
Similarities in Classes
247
class Falling {
int x;
int y;
int deltaY;
IColor c;
class Star {
Star(int y) {
super(20,y,5,new Yellow());
}
Star drop() {
if (this.landed()) {
return this; }
else { if (this.nearGround()) {
return
new Star(100); }
else {
return
new Star
(this.y + this.deltaY );}
}
}
RedRock(int x,int y) {
super(x,y,5,new Red());
}
RedRock drop() {
if (this.landed()) {
return this; }
else { if (this.nearGround()) {
return
new RedRock(this.x,100); }
else {
return
new RedRock(this.x + this.deltaX,
this.y + this.deltaY); }
}
}
boolean hit(Girl g) {
return
g.closeTo(new Posn(this.x,this.y));
}
Section 18
248
inside of RedRock :
RedRock drop() {
if (this.landed()) {
return this; }
else { if (this.nearGround()) {
return
new RedRock (this.x,100); }
else {
return
new RedRock
(this.x + this.deltaX,
this.y + this.deltaY); }
}
}
problem is that Javas type systemand that of almost all mainstream objectoriented programming languagesis overly restrictive. This kind of problem is the sub-
Similarities in Classes
249
Exercises
Exercise 18.16 Use the test suite you developed in exercise 18.14 to test the
program in figure 79.
Exercise 18.17 Develop an interface for the newly created union of Stars
and RedRocks. Does drop fit in? Why? Why not?
Exercise 18.18 Modify the code in figure 79 so that the drop methods become identical except for the return types and the class names in new expressions.
Once we can draw shapes onto a canvas, we can simulate the movement
of objects, too:
class ExampleMove {
Canvas c = new Canvas(100,100);
Star f = new Star(10);
ExampleMove() { }
boolean testDraw = check this.c.show()
&& this.f.draw(this.c)
&& this.f.drop().draw(this.c)
&& this.f.drop().drop().draw(this.c)
expect true;
This examples class creates two fields: a Canvas and a Star. Its one and only
test field displays the canvas, draws the Star, drops and draws it again,
and finally drops it twice and draws the result. At that point, the Canvas
contains three yellow circles, drawn at (10,20), (10,25), and (10,30).
Imagine now the addition of a method drawBackground, which draws a
white rectangle of 100 by 100 pixels at position (0,0) onto the canvas. Doing
so every time before draw is called leaves just one yellow circle visible. If
the method calls happen at a fast enough rate, the series of pictures that
ExampleMove draws in this manner creates the illusion of a moving picture:
a movie. A library that helps you achieve this kind of effect is a major
example in the next section.
ject of programming languages researchers. One of their objectives is to design powerful
yet safe type systems for languages so that programmers can express their thoughts in a
concise, abstract and thus cost-effective manner.
Section 18
250
+-------------------------+
| Posn
|
+-------------------------+
| int x
|
| int y
|
+-------------------------+
|
/ \
--|
|
============================
|
|
|
+---------------------------------+
| CartPt
|
+---------------------------------+
+---------------------------------+
| double distanceTo()
|
| double distTo0()
|
| CartPt translate(int dX, int dY)|
+---------------------------------+
251
Section 19
252
class Examples {
CartPt origin = new CartPt(0,0);
CartPt other = new CartPt(3,4);
boolean test =
CartPt(int x, int y) {
this.x = x;
this.y = y;
}
check
this.origin.distanceTo(this.other)
expect 5.0
within .001;
Examples() {}
( x1 x2 ) 2 + ( y1 y2 ) 2
253
Section 19
254
distanceTo; the third one just exists for bridging the gap between distanceTo
and distance0.
An alternative to the addition of a method is the definition of LOCAL
VARIABLES:
inside of CartPt :
double distanceTo(CartPt other) {
int deltaX = this.x other.x;
int deltaY = this.y other.y;
CartPt p = new CartPt(deltaX,deltaY);
return p.distance0();
}
Here we see three of them: deltaX, deltaY, and p. At first glance, the definition of a local variable looks like a field definition with an initialization
equation. The difference is that a local variable definition is only visible
in the method body, which is why it is called local.
In distanceTo, the local variables are initialized to the following values:
1. deltaX stands for the difference between the x coordinates;
2. deltaY stands for the difference between the y coordinates;
3. and p is a point created from the two differences.
As these definitions show, the expression on the right-hand side of initialization equations can use a number of values: those of the classs fields,
the parameters, the fields of the parameters, and local variables whose definitions precedes the current one. Like fields, the name of the variable
stands for this value, though only through the body of the method.
Based on this explanation of local variables, we can also perform a
calculation to validate the equivalence of the intermediate version of distanceTo and the third one:
1. Since deltaX stands for the value of this.x other.x, we can replace the
occurrence in the third initialization equation:
inside of CartPt :
double distanceTo(CartPt other) {
int deltaY = this.y other.y;
CartPt p = new CartPt(this.x other.x,deltaY);
return p.distance0();
}
255
Exercises
Exercise 19.1 Can you argue that the second and first version of distanceTo
are equivalent?
Exercise 19.2 Transform the CartPt class provided in figure 81 into the second and then the third version, maintaining the Examples class as you go.
Section 19
256
class LightSwitch {
boolean on;
int width = 100;
int height = this.width;
int radius = this.width/2;
Canvas c = new Canvas(this.width,this.height);
Posn origin = new Posn(0,0);
Posn center = new Posn(this.radius,this.radius);
IColor light = new Yellow();
IColor dark = new Blue();
LightSwitch(boolean on) {
this.on = on;
this.c.show();
}
// turn this switch off, if on; and on, if off
LightSwitch flip() {
return new LightSwitch(!this. on);
}
// draw this light
boolean draw() {
if (on) {
return this.paintOn(); }
else {
return this.paintOff (); }
}
boolean paintOff () {
boolean paintOn() {
257
}
Given that each of these methods is used only once in draw, we can just
change its definition by replacing the invocations of paintOn and paintOff
with their bodies:
Section 19
258
inside of LightSwitch :
boolean draw() {
if (on) {
return this.paint(this.light,this.dark); }
else {
return this.paint(this.dark,this.light); }
}
Now all that remains is to run some examples and inspect the appearances
of the canvases before and after abstracting.
Exercise
Exercise 19.3 Develop an example class for figure 82 and then transform
the class to use the paint abstraction. Why is this not a proper test?
+---------------+
| IClass
|
+---------------+
| m
|
+---------------+
|
/ \
--|
... ----------------- ...
|
|
+------+
+---------+
| One |
| Another |
+------+
+---------+
| f
|
| f
|
+------+
+---------+
|
|
|
|
| m(x) |
| m(x)
|
| .x. |
| .x.
|
+------+
+---------+
+---------------+
+---------------+
| IClass
|__/|____| AClass
|
+---------------+ \|
+---------------+
| m
|
| f
|
+---------------+
+---------------+
| m(x)
|
| .x.
|
|
|
+---------------+
|
/ \
--|
... ----------------- ...
|
|
+------+
+---------+
| One |
| Another |
+------+
+---------+
+------+
+---------+
|
|
|
|
+------+
+---------+
259
The comparison After you have finished designing a union hierarchy, look
for identical field definitions or method definitions in the variants
(subclasses). The fields should occur in all variants; the methods
should be in at least two variants.
The abstraction Create the abstract class AClass and have it implement the
union interface with abstract methods. Replace the implements specification in the subclasses with extends specifications, pointing to the
superclass.
Eliminate the common field definitions from all variants; add a single
copy to the superclass. Modify the constructors in the subclasses to
use super for the initialization of shared fields.
Introduce a copy of the shared method m in the superclass and eliminate the methods from all those subclasses where the exact same code
appears.
The super call For all those variants that contain a method definition for m
that differs from the one in the superclass, consider reformulating the
method using super. We have seen an example of this in section 18.3,
and it is indeed a reasonably common scenario. If it is possible, express the method in this manner because it helps your successor understand the precise relationship between the two computations.
As long as you do not have sufficient experience with abstraction in
an object-oriented setting, you may wish to skip this step at first and
focus on the basic abstraction process instead.
The test Re-run the test suite. Since subclasses inherit the methods of the
abstract class that represents the union, the subclasses did not really
change, especially if you skipped the third step. Still, every modification may inadvertently introduce a mistake and therefore demands
testing. Since you have a test suite from the original development,
you might as well use it.
This first design recipe for abstraction is extremely simple in comparison to the one we know from How to Design Programs. The reason is that it
assumes that the methods in the two variants are identical. This assumption is is of course overly simplistic. In general, methods are similar, not
identical. If this is the case, you can often factor out the common pattern
between those methods and then lift the common method.
Figure 84 is a sketch of the first step in this modified abstraction process.
Its left column indicates that two variants of a union contain similar method
Section 19
260
+---------------+
+---------------+
| IClass
|__/|__| AClass
|
+---------------+ \| +---------------+
| m
|
+---------------+
+---------------+
| abstract m
|
|
|
+---------------+
|
/ \
--|
... ---------- ...
|
|
+------+
+---------+
| One |
| Another |
+------+
+---------+
+------+
+---------+
|
|
|
|
|m(x) |
|m(x)
|
| .x.o.|
| .x.p.
|
|
|
|
|
+------+
+---------+
+---------------+
+---------------+
| IClass
|__/|__| AClass
|
+---------------+ \| +---------------+
| m
|
+---------------+
+---------------+
| abstract m
|
|
|
+---------------+
|
/ \
--|
... ---------- ...
|
|
+-------+
+---------+
| One
|
| Another |
+-------+
+---------+
+-------+
+---------+
|
|
|
|
|n(x,y) |
|n(x,y)
|
| .x.y. |
| .x.y.
|
|m(x)
|
|m(x)
|
| n(x,o)|
| n(x,p) |
|
|
|
|
+-------+
+---------+
definitions. More specifically, the definitions differ in one place, where they
contain different values: o and p, respectively. The right column shows
what happens when we apply the design recipe for abstraction from How
to Design Programs. That is,
The comparison Highlight the differences between m in class One and m
in class Another.
The abstraction Define method n just like method m (in both classes) with
two differences: it specifies one additional parameter (y) and it uses
this parameter in place of the original values (p and o).
The test, 1 Reformulate the body of m in terms of n; more precisely, m now
calls n with its arguments and one extra argument: o in One and p in
Another.
The test, 2 Re-run the test suite to confirm that the m methods still function
as before. You may even want to strengthen the test suite so that it
covers the newly created method n.
At this point you have obtained two identical methods in the two classes.
You can now follow the design recipe for abstraction to lift n into the common superclass.
A similar design recipe applies when you discover that the variants contain fields with distinct names but the same purpose. In that case, you first
261
rename the fields consistently throughout one class, using the corresponding field name from another variant. You may have to repeat this process
until all names for fields with the same purpose are the same. Then you lift
them into the common superclasses. Of course you can only do so if you
have complete control over the classes, i.e., if you are in the middle of the
design process, or if you are guaranteed that other pieces of the program
use the interface of the union as the type and do not use individual variant
classes. Otherwise people may already have exploited the fact that your
classes contain fields with specific names. In this case, you are stuck and
cant abstract anymore without major problems.
+---------+
| ClassAA |
+---------+
| f . g
|
+---------+
|
|
| m ...
|
|
|
+---------+
+---------+
| ClassBB |
+---------+
| f . g
|
+---------+
|
|
| m ...
|
|
|
+---------+
+---------------+
+---------+
| IClass
|__/|__| SuperCC |
+---------------+ \| +---------+
| m
|
| f . g
|
+---------------+
+---------+
|
|
| m ...
|
|
|
+---------+
|
/ \
--|
+----------+
|
|
+---------+
+---------+
| ClassAA |
| ClassBB |
+---------+
+---------+
+---------+
+---------+
|
|
|
|
|
|
|
|
+---------+
+---------+
some object-oriented languages, a class may have multiple superclasses and then
262
Section 19
263
// a bag of integers
class Bag {
ILin elements;
Bag(ILin elements) {
this.elements = elements;
}
Set(ILin elements) {
this.elements = elements;
}
boolean in(int i) {
int howMany(int i) {
return
this.elements.howMany(i) > 0;
return
this.elements.howMany(i);
Exercises
Exercise 19.4 Compare the two classes in figure 86. A Set is a collection of
integers that contains each element at most once. A Bag is also a collecion
of integers, but an integer may show up many times in a bag.
Section 19
264
265
The two classes obviously share a lot of code. Abstract a common superclass, say AMoney, from the two classes. Lift as many methods and fields
as possible.
Add a class Pound for representing amounts in British Pounds.
Library:
Library:
+-------------------------+
| LibCC
|
+-------------------------+
| /////
|
| ...
|
+-------------------------+
| +++++
|
+-------------------------+
============================
+-------------------------+
| AA
|
+-------------------------+
| /////
|
| ...
|
+-------------------------+
| +++++
|
| ...
|
+-------------------------+
+-------------------------+
| LibCC
|
+-------------------------+
| /////
|
| ...
|
+-------------------------+
| +++++
|
+-------------------------+
/ \
--|
|
|
============================
|
|
|
+-------------------------+
| AA
|
+-------------------------+
| ...
|
+-------------------------+
| ...
|
+-------------------------+
1. When your task is to design a class, follow the recipe for designing
a class and its methodsup to a certain point. At a minimum, write
down the purpose statement for the class, the fields for the class, and
some sample instances. Then develop the signatures and purpose
statements of the methods; ideally, you should also develop examples
for the methods.
266
Section 19
Going through the design recipe in this way helps you understand
what this new class of data is all about. Now you are in a position
where you can determine whether this class should be an extension
of an existing class. Here are two situations that suggest such an extension:
(a) You already have a class that has a related but more general purpose statement. Also, the existing class provides several of the
fields and methods that you need for the new class, if not by
name then in spirit.
The derivation of Precipitation from ARecording exemplifies this
situation. It represents the extension of an existing union of
classes with an additional variant. This step is common, because
we often dont think of all the variants when we first design a
union.
Note: When we extend a class that is not a union, we often really
want a union not just one variant of an existing class.
(b) The collections of software that are available to you on your
computer or at your workplace contain a potential superclass
LibCC whose purpose fits the bill. Furthermore, the collection
contains other classes that rely on LibCC. If you derive your new
class from LibCC, you benefit not just from the fields and methods in the class but also from methods in other classes in this
collection.
Deriving CartPt from Posn in the drawing package is an instance
of this situation. As mentioned, every instance of CartPt can now
play the role of a Posn when it comes to drawing an object that
needs more methods on positions than Posn offers.
Note: In many cases, such libraries are designed to be extended.
Some times they provide abstract classes; the abstract methods
in these classes specify what you need to define yourself. At
other times, the classes are also fully functional, with methods
that specify some reasonable default behavior. In this case, the libraries tend to come with specific prescriptions of what the programmer must override to exploit the functionality.
In both cases, the superclass is outside of your reach, that is, you cant
modify it. Because this is most common with so-called software libraries we dub this situation the library class situation.
267
Section 19
268
the ceiling of the world, the game is over. The objective of this
game is to land as many blocks as possible.
The two screen shots illustrate the idea. In the left one, you
can see how one block, the right-most one, has no support; it
is falling. In the right one, the block has landed on the ground;
an additional block has landed on top of the stack near the left
perimeter of the screen shot. This stack now reaches the top of
the canvas, which means that the game is over. . . .
A Tetris program definitely needs to represent blocks. Hence your first
task is to design a class for this purpose. These blocks obviously need to
include information about their location. Furthermore, the game program
must be able to draw the blocks, so the class should come with at least one
method: draw, which draws a block on a canvas at the appropriate location.
+---------------------------+
| Block
|
+---------------------------+
| int down
|
| int right
|
+---------------------------+
| boolean draw(Canvas w)
|
+---------------------------+
+---------------------------+
| Block
|
+---------------------------+
| int down
|
| int right
|
+---------------------------+
| boolean draw()
|
+---------------------------+
|
/ \
--|
|
+---------------------------+
| DrpBlock
|
+---------------------------+
| int deltaY
|
+---------------------------+
| DrpBlock drop()
|
| boolean landed(IBlocks r) |
| DrpBlock steer(String ke, |
|
IBlocks r) |
+---------------------------+
269
fields, which record how far down (from the top) and how far to the right
(from the left border) the block is. The draw method consumes the canvas
into which it is supposed to draw; all other information comes from the
block itself.
Exercises
Exercise 19.6 Design the Block class, including a draw method. Include an
example class with a test field that demonstrates that the draw method functions.
Exercise 19.7 Instances of Block represent blocks resting on the ground or
on each other. A game program doesnt deal with just one of those blocks
but entire collections. Design a data representation for lists of blocks. Include a method that can draw an entire list.
Of course, the Block class isnt enough to play the game. First, your
game program needs lists of such blocks, as exercise 19.7 points out. Second, the problem statement itself identifies a second class of blocks: those
that are in the process of dropping down to the ground. Since we already
have Block for representing the class of plain blocks and since a dropping
block is like a block with additional properties, we next consider deriving
an extension of Block.
The derived class has several distinguishing properties. The key distinction is that its instances can drop. Hence, the class comes with a method
that simulates the drop and possibly an additional field that specifies how
fast an instance can drop as a constant:
1. drop, a method that creates a block that has dropped by some pixels;
2. deltaY, a field that determines the rate at which the block drops.
Furthermore, a dropping block can land on the ground or on the blocks
that are resting on the ground and the player can steer the block to the left
or right. This suggests two more methods:
3. landed, which consumes a list that represents the resting blocks and
determines whether this block has landed on the ground or one of
the other blocks;
Section 19
270
4. steer, which also consumes the list of resting blocks and a String, and
moves the block left or right. Whether or not the block can move in
the desired direction depends of course on the surrounding blocks.
The right column of figure 88 displays the revised class diagram. It
consists of two concrete classes, with Block at the top and DrpBlock, the
extension, at the bottom. The diagram assumes the existence of IBlocks, a
representation of a list of blocks.
Exercises
Exercise 19.8 Define the DrpBlock class and design all four methods on the
wish list.
Hint: You may wish to employ iterative refinement. For the first draft,
assume that the list of resting blocks is always empty. This simplifies the
definitions of landed and steer. For the second draft, assume that the list of
resting blocks isnt empty. This means that you need to revise the landed
and steer methods. As you do so, dont forget to maintain your wish list.
Exercise 19.9 Add test cases to your example class from exercise 19.8 that
exercise the draw methods of Block, DrpBlock, and IBlocks.
At first glance, the derivation of DrpBlock from Block is a natural reflection of reality. After all, the set of dropping blocks is a special subset of the
set of all blocks, and the notion of a subclass seems to correspond to the notion of a subset. A moments thought, however, suggests another feasible
program organization. If we think of the set of all blocks as one category
and the set of all dropping blocks as a special kind of block, the question
arises what to call all those blocks that are not dropping. As far as the game
is concerned, the remaining blocks are those that are already resting on the
ground and each other. Put differently, within the collection of blocks there
are two kinds of blocks: the resting ones and the dropping ones; and there
is no overlap between those two sets of blocks.
Figure 89 translates this additional analysis into a modified class diagram. The class hierarchy has become a conventional union arrangement.
In particular, Block has become an abstract class with two concrete subclasses: Resting and DrpBlock. The diagram also shows that DrpBlock comes
with one additional method: onLanding, which produces an instance of
Resting from a DrpBlock as the dropping block lands on the ground or on
one of the resting blocks.
271
+---------------------------+
| ABlock
|
+---------------------------+
| int down
|
| int right
|
+---------------------------+
| boolean draw(Canvas c)
|
+---------------------------+
|
/ \
--|
---------------|
|
+---------+
+--------------------------+
| Resting |
| DrpBlock
|
+---------+
+--------------------------+
+---------+
| Resting convert()
|
| DrpBlock drop()
|
| boolean landed(IBlocks r)|
| DrpBlock
|
| steer(String ke,
|
|
IBlocks r)
|
| Resting onLanding()
|
+--------------------------+
An interesting aspect of the revised diagram is that Resting comes without any additional fields or methods. All the features it needs are inherited
from ABlock, where they are defined because they are also needed in DrpBlock. Still, representing blocks as a union of two disjoint classes has an
advantage. Now IBlocks can represent a list of resting blocks, that is, it is a
list of instances of Resting rather than Block. Using Resting instead of Block
signals to the (future) reader that these blocks cannot move anymore. No
other part of the program can accidentally move these resting blocks anymore or perform some operation on them that applies to dropping blocks
only. Furthermore, as an instance of DrpBlock lands, it must become a member of the list of Resting blocks. This can only happen, however, if it is first
converted into an instance of Resting. Thus, it is totally clear to any reader
that this block has ceased to move.
In summary, the development of a subclass is often justified when we
can identify a special subset of the information that we wish to represent.
Pushing this analysis further, however, tends to reveal advantages of the
creation of a full-fledged union rather than a single subclass. Always consider this option when you believe you must derive a subclass from an
existing class in your program.
Exercises
Exercise 19.10 Develop an interface for ABlock for the methods that all vari-
Section 19
272
273
A drawing library such as draw has the proper ingredients for creating
the illusion of an item dropping from the top to the bottom of a canvas.
Recall from the end of the previous section that on a computer canvas, a
dropping block is a block that is drawn over and over again at different
positions on a brand new background. Consider these two images:
In the right image, the block appears at a lower place than in the left one. If
the transition from left to right is fast, it appears as if the block is moving.
Hence, drawing the background, drawing a block on this background, and
repeating those actions in a predictable rhythm is our goal.
Drawing libraries such as draw abstract over this pattern of repetition
with a mechanism that allows something to happen on every tick of a clock.
To this end, our draw package provides an additional abstract class: World.
Figure 90 contains the outline of Worlds definition. It provides one field
(theCanvas), two methods (bigBang, endOfWorld) and three abstract methods. The field refers to the canvas on which the world appears. The first
concrete method, bigBang creates the world, makes the canvas appear, and
starts the clock, and makes it click every s seconds. The second one, endOfWorld stops the clock and thus all animations. The three abstract methods
in World are onTick, onKeyEvent, and draw. The first two are called EVENT
HANDLERS because they react to events in the world. The onTick method
is invoked for every tick of the clock; it returns a new world. Similarly, onKeyEvent is invoked on the world and a keystroke, every time a person hits
a key on the keyboard; the String parameter specifies which key has been
hit. Like onTick, onKeyEvent returns a new world when it is done. The draw
method draw this world.
The purpose of World is to represent animated mini-worlds. To use it,
you define a subclass of World. Furthermore, since World itself is abstract,
you must override the four abstract methods if you want to create worlds.
Then, as soon as a call to bigBang starts the clock, the Worlds canvas becomes alive. At each clock tick, the code in the library calls your onTick to
create the next world and then uses draw for re-drawing this new world.
The process continues until the clock is stopped, possibly with endOfWorld.
Section 19
274
In short, your subclass and World collaborate according to the templateand-hook pattern.
The following table illustrates the workings of onTick graphically:
clock tick
this current world
result of this.onTick()
0
a
b
1
b
c
2
c
d
...
...
...
n
w
x
n+1
x
...
The first row denotes the number of clock ticks that have happened since
a.bigBang() started the clock. The invocation of a.onTick() produces b, which
becomes the current world. This means, in particular, that the next clock
tick triggers a call to onTick in b. In general, the result of the nth call to
onTick becomes the n + 1st world.
So, if your goal is to create an animated block world where a single
block drops to the bottom of the canvas, you must define a class, say BlockWorld, that extends World and overrides: onTick, onKeyEvent, and draw. The
simplest such subclass is sketched in figure 91:
1. the field block of type DrpBlock (see previous section) refers to the one
block that is dropping in this world;
2. onTick drops the block and creates a new world from the resulting
block;
3. draw draws the block onto this worlds canvas via the blocks draw
method and its own drawBackground method; and
4. onKeyEvent returns just this world.
The last point is surprising at first, but remember that without a definition
for onKeyEvent BlockWorld would be an abstract class. The definition in
figure 91 means that hitting a key has no effect on the current world; it is
returned as is.
Now that you have an extension of World with working onTick, and draw
methods you can run your first animation:
DrpBlock aBlock = new DrpBlock(10,20);
World aWorld = new BlockWorld(aBlock);
aWorld.bigBang(aWorld.WIDTH,aWorld.HEIGHT,.1)
The first line creates the representation of a block, located 10 pixels to the
right from the origin and 20 pixels down. The second one constructs a
BlockWorld with this DrpBlock. The last one finally starts the clock and thus
275
the mechanism that calls onTick and draw every .1 seconds. If you want the
block to move faster, shorten the time between clock ticks. Why?
This first success should encourage you for the next experiment. Moving an object each tick of the clock is only one possibility. In addition, we
can also have the object react to keystrokes. Remember that our dropping
blocks from the preceding section should also be able to move left and right.
Hitting the right arrow key should move the block to the right, and hitting
the left arrow key should move it to the left. As the documentation for
World says, each such keystroke triggers an invocation of the onKeyEvent
Section 19
276
+----------------------+
+---------------------------+
+----------------------+
| Canvas
|<-+ | World
| # | Block
|
+----------------------+ | +---------------------------+ # +----------------------+
| boolean drawRect(Posn) +--| Canvas theCanvas
| # | int down
|
+----------------------+
+---------------------------+ # | int right
|
| World onTick()
| # +----------------------+
| World onKeyEvent(String k)| # | boolean draw(Canvas c)|
| boolean draw()
| # +----------------------+
+----------+
+---------------------------+ #
|
| Posn
|
|
#
|
+----------+
|
#
|
| int x
|
|
#
|
| int y
|
|
#
|
+----------+
/ \
#
/ \
--#
--+--------+
|
#
|
| IColor |
|
#
|
+--------+
|
#
| +-----------------------+
|
|
#
| | IBlocks
|
/ \
|
#
| +-----------------------+
--|
#
| | boolean draw(Canvas c)|
|
|
#
| | boolean atop(Block b) |
... ------------------------ ...
|
#
| | boolean left(Block b) |
|
|
|
|
#
| | boolean right(Block b)|
...
...
...
|
#
| +-----------------------+
|
#
|
============================================================#
|
|
|
+---------------------------+
|
| BlockWorld
|
+-----------------------+
+---------------------------+
+--->| DrpBlock
|
| DrpBlock block
|----+
+-----------------------+
| IColor BACKG
|
| int deltaY
|
+---------------------------+
+-----------------------+
| World onTick()
|
| DrpBlock drop()
|
| World onKeyEvent(String k)|
| DrpBlock steer(
|
| boolean draw()
|
| String ke)
|
| boolean drawBackground() |
+-----------------------+
+---------------------------+
277
as it drops.
From here it is a short step to a program that drops many blocks, lands
them on the ground and each other, and allows the player to move the currently dropping block left or right. Figure 92 sketches the class diagram
for this program, including both the library and the block world classes.
Both parts consist of many interfaces and classes. On our side, you see
Block and DrpBlock as designed in the previous section; BlockWorld specializes World to our current needs across the library boundary. For simplicity,
the diagram omits the color classes in draw and the context of blocks on
which an instance of DrpBlock may land. Refer to this diagram as you solve
the following exercises and finish this simple game.
Exercises
Exercise 19.12 Finish the class definitions for the block world program.
Start with drawBackground; it is a method that colors the entire background
as needed. Change the colors of the world and the block.
Exercise 19.13 Modify BlockWorldand all classes as neededso that the
animation stops when the bottom of the dropping block touches the bottom
of the canvas. Hint: Check out endOfWorld from draw.
Exercise 19.14 Add a list of resting blocks to BlockWorld based on your implementation of IBlocks in exercise 19.11. The program should then draw
these blocks and land the dropping block either on the bottom of the canvas or on one of the resting blocks. When the player steers, make sure that
the dropping block cannot run into the resting block.
Exercise 19.15 Design the class Blink, which represents blinking objects. A
blinking object is a 100 by 100 square that displays two different colors at
a rate of one switch per second. The Blink method should contain a run
method that starts the clock and the blinking process.
Hint: Naturally Blink is an extension of World.
Your example class should test the onTick method before it invokes the
run method expecting true. It should also create at least two distinct instances of Blink and display them at the same time.
Section 19
278
that represent the various pieces of the game and methods that can draw
them or move them one step at a time. What is missing from the game
is real action: a UFO that is landing; shots that are flying; an AUP that is
moving. Since we now know how this works, its time to do it.
+------------------------+
# +------------------>| IShots
|<--------------------+
# |
+------------------------+
|
# |
| IShots move()
|
|
# |
| boolean draw(Canvas c) |
|
# |
| boolean hit(UFO u)
|
|
# |
+------------------------+
|
# |
|
|
# |
/ \
|
# |
--|
# |
|
|
# |
------------------------------|
|
# |
|
|
|
/ \
# |
+------------------------+
+------------------------+
|
--# |
| MtShots
|
| ConsShots
|
|
|
# |
+------------------------+
+------------------------+
|
========================= |
| Shot first
|----+
|
|
| IShots rest
|----+
|
|
+------------------------+
|
|
|
|
+------------------+
|
|
| UFOWorld
|
|
|
+------------------+
|
|
| int WIDTH
|
|
|
| int HEIGHT
|
|
v
| IColor BACKG
|
|
+------------------------+
+------------------------+
| UFO ufo
|----|-------------> | UFO
|
| Shot
|
| AUP aup
|----|---+
+------------------------+
+------------------------+
| IShots shots
|----+
|
| IColor colorUFO
|
| IColor colorShot
|
+------------------+
|
| Posn location
|
| Posn location
|
| World move()
|
|
+------------------------+
+------------------------+
| ???
|
|
| UFO move()
|
| Shot move()
|
+------------------+
|
| boolean draw(Canvas c) |
| boolean draw(Canvas c) |
|
| boolean landed()
|
| boolean hit(UFO u)
|
|
| boolean isHit(Posn s) |
+------------------------+
|
+------------------------+
v
+------------------------+
| AUP
|
+------------------------+
| IColor aupColor
|
| int location
|
+------------------------+
| AUP move()
|
| boolean draw(Canvas c) |
| Shot fireShot()
|
+------------------------+
+------------------+
| World
|
+------------------+
| Canvas theCanvas |
+------------------+
| World onTick()
|
| World onKeyEvent(|
| String ke)
|
| boolean draw()
|
+------------------+
279
specific case, that the subclass must override onTick, onKeyEvent, and draw
to achieve the desired effects.
Of these to-be-overridden methods we already have draw:
inside of UFOWorld :
boolean draw(Canvas c) {
return
c.drawRect(new Posn(0,0),this.WIDTH,this.HEIGHT, this.BACKG)
&& this.ufo.draw(c) && this.aup.draw(c) && this.shots.draw(c);
}
We developed this method in section 16.3 just so that we could see what the
world of UFOs looks like. Its definition reflects our thinking back then that
a drawing method must consume an instance of Canvas. Now we know,
however, that a UFOWorld is also a World, which already contains a canvas:
inside of UFOWorld :
boolean draw() { // (version 2)
return
this.theCanvas
.drawRect(new Posn(0,0),this.WIDTH,this.HEIGHT,this.BACKG)
&& this.ufo.draw(this.theCanvas)
&& this.aup.draw(this.theCanvas)
&& this.shots.draw(this.theCanvas);
}
Since draw is defined within UFOWorld, it has access to this inherited canvas and can use it to display the world. Furthermore, it passes this canvas
to the draw methods of the various objects so that they can draw themselves
onto the canvas.
Exercise
Exercise 19.16 The draw method can benefit from local variable declarations, especially if the names are suggestive of the desired action:
inside of UFOWorld :
boolean draw() { // (version 2, with local variables)
boolean drawBgd = this.theCanvas
.drawRect(new Posn(0,0),this.WIDTH,this.HEIGHT,this.BACKG);
boolean drawUFO = this.ufo.draw(this.theCanvas);
...
return true;
}
Section 19
280
Explain how this version of the method works. Reformulate the rest of the
method and use it to ensure it still works.
281
nition. Its purpose is to compute the next world. In our case study, this
next world is a world in which the UFO has dropped a little bit more and
in which the shots have risen a little bit, too. Except that if the UFO has
landed or if one of the shots has hit the UFO, the game is over. Put differently, the method must distinguish three different scenarios:
in the first scenario, the UFO has reached the ground and has landed;
in the second scenario, one of the shots has gotten close enough to the
UFO to destroy it;
the third scenario is the typical one; the UFO didnt land and no shot
got close to it.
Turning this case distinction into a method is routine work. Still it is
worth considering how to get to its definition from the template:
inside of UFOWorld :
World onTick() {
return . . . this.ufo.move() . . . this.aup.move() . . . this.shots.move()
}
The templates expressions remind us that a UFOWorld consists of a ufo, an
aup, and a list of shots. Each of these comes with its own methods, which
the template doesnt indicate here but it reminds us of this fact. When we
consider the purpose statement and our discussion of it, however, everything makes sense. We can use this.ufo.landed() to find out whether we are
in the first scenario. Similarly, if an invocation of this.shots.hits yields true,
we know that we are in the second scenario. Lastly if neither of these conditions hold, everything proceeds normally. In the first two cases, the world
stops; in the last one all the objects move along.
Figure 94 displays the complete definition of onTick. At this point, the
class definition is complete enough to observe the animation. Specifically,
the UFO should descend and the shots should fly, though the AUP wont
react to keystrokes yet. Try it out! (Dont forget to supply a simplistic definition of onKeyEvent.)
With a first success under our belt, we can turn our attention to the
players actions. So far, your manager imagines these actions:
1. hitting the up-arrow key fires a shot;
2. hitting the left or right arrow key moves the AUP to the left or right;
3. and hitting any other key affects nothing.
Section 19
282
While the second kind of action concerns the AUP, the first one uses the
AUP and affects the collection of shots as a whole.
Here is the template that distinguishes these three arguments:
inside of UFOWorld :
World onKeyEvent(String k) {
if (k.equals("up")) {
return . . . this.ufo . . . this.aup . . . this.shots; }
else { if (k.equals("left") || k.equals("right")) {
return . . . this.ufo . . . this.aup . . . this.shots; }
else {
return . . . this.ufo . . . this.aup . . . this.shots; }
}
}
As before, the expressions in the branches remind us of what data to which
the method has access.
The rest of the development proceeds as before:
1. In the first scenario, the player hits the up-arrow key, which means
that onKeyEvent has to use the shoot method for firing a shot.
2. In the second scenario, the player hits a left-arrow key or a rightarrow key; in that case, the key event is forwarded to the AUP, which
interprets the key stroke and moves the AUP appropriately.
3. Otherwise, nothing happens.
The resulting method definition is included in figure 94.
The game is ready for you to play. Even though it is a minimalist game
program, it does display a canvas; the UFO is landing; the shots are flying;
and the AUP is moving under your control. You can win and, if you really
want and try hard, you can lose. Enjoy, and read the rest of the section to
find out how you can add some spice to the game.
Exercises
Exercise 19.17 Collect all code fragments for the classes UFOWorld, UFO,
AUP, IShots, MtShots, and ConsShots.
Develop tests for the onTick and onKeyEvent methods. That is, create
simple worlds and ensure that onTick and onKeyEvent create correct worlds
from them.
283
Add the following line to your Examples class from figure 34:
inside of Examples :
boolean testRun = check this.w2. bigBang(200,500,1/10) expect true;
This displays a 200 by 500 canvas; starts the clock; and ticks it every 1/10th
of a second. You should see the UFO descend and, when it gets close
enough to the ground, you should see it land. The two shots of w2 should
fly. If you press the up arrow, you should see a shot leaving the AUP. Watch
it fly. Add another shot. And another one. Now press a left or a right arrow
key. The AUP should move in the respective direction.
Lastly, add a run method to UFOWorld so that you can run the game
easily from the interactions window.
Exercise 19.18 As is, the game is rather boring. The UFO descends in a
straight manner, and the AUP shoots straight up. Using random numbers,
you can add an element of surprise to the game.
Modify the method move in UFO so that the object zigzags from left to
right as it descends. Specifically, the UFO should move to the left or right
by between 0 and 3 pixels every time it drops a step. As the UFO swerves
back and forth, make sure that it never leaves the visible part of the world.
Think hard as you create examples and reasonable tests. Even though you
cannot predict the precise result once you add randomness, you can predict
the proper range of valid results.
284
Section 19
ProfessorJ and Java provide random values via the Random class. Figure 95 displays the relevant excerpt from this class for this exercise. You can
use it after you import the package java.util.Random just like you import
geometry and colors.
Hint: You may wish to employ iterative refinement. For the first draft,
design a UFO that swerves back and forth regardless of the worlds boundaries. For the second draft, design an auxiliary method that tests whether
the new UFO is outside the boundaries; if so, it rejects the movement and
tries again or it just eliminates the side-ways movement. Note: the technique of generating a candidate solution and testing it against some criteria
is useful in many situations and naturally calls for method composition. In
How to Design Programs, we covered it under generative recursion.
Exercise 19.19 In addition to random movements as in the preceding exercise (19.18), you can also turn the AUP into something that acts like a
realistic vehicle. In particular, the AUP should move continuously, just like
the UFO and the shots.
Implement this modification. Let the design recipe guide you. Hint:
Start by removing the move method from the AUP class and add a speed
field to the AUP. A positive speed moves the AUP to the right; a negative
speed moves it left.
The meaning of a key stroke changes under this scenario. Hitting the
left or right arrow key no longer moves the vehicle. Instead, it accelerates
and decelerates the vehicle. For example, if the AUP is moving to the right
and the player hits the right arrow key, then the AUPs speed should increase. If the player hits the left key, the AUPs speed should decrease.
Exercise 19.20 During a brainstorming session, your team has come up
with the idea that a UFO should defend itself with AUP-destroying charges.
That is, it should drop charges on a random basis and, if one of these
charges hits the AUP, the player should lose the game.
Implement this modification after completing exercise 19.18. A charge
should descend at twice the speed of the UFO, straight down from where
it has been released.
Now that we have a tested, running program, the time has come to edit
it. That is, we look for replications, for chances to eliminate common patterns, for opportunities to create a single point of control. The best starting
point for this exercise is always the class diagram. Start with a look inside unions for common fields and methods; create a common superclass
285
where possible. Then compare classes that have similar purposes and appearances; check whether introducing a union makes sense.
In the War of the Worlds program, there is little opportunity for either
kind of abstraction. It definitely lacks a union that can benefit from lifting
fields and code, though the revised diagram (see figure 93) suggests one
pair of similar classes: UFO and Shot. Both contain two fields with the
same type. We also know that both contain draw and move methods, so
perhaps they deserve some cleanup. For the exercises sake, lets see how
far abstraction takes us here.
// represent a descending UFO
+-------------------------+
| UFO
|
+-------------------------+
| Posn location
|
| IColor colorUFO
|
+-------------------------+
| boolean draw(Canvas c) |
| UFO move()
|
+-------------------------+
Section 19
286
+---------------------------------+
| AMovable
|
+---------------------------------+
| Posn location
|
| IColor color
|
+---------------------------------+
| abstract boolean draw(Canvas c) |
+-------------------------------- +
|
/ \
--|
+-------------------------+
|
|
+------------+
+-------------+
| UFO
|
| Shot
|
+------------+
+-------------+
| UFO move() |
| Shot move() |
+------------+
+-------------+
class UFO {
UFO(Posn loc) {
super(loc,new Green());
}
class Shot {
Shot(Posn loc) {
super(loc,new Yellow());
}
The gray-shaded expressions stand out because they are nearly identical
and suggests an abstraction. Both produce a new location, a Posn to be
precise, for the new objects. Hence, we should naturally think of actually
adding a method to Posn that accomplishes this translation of a point.
Although we cannot modify Posn itself, because it belongs to the unmodifiable draw package, we can create a subclass of Posn that provides
this service:
class Location extends Posn {
Location(int x,int y) { super(x,y); }
287
In turn, AMovable should use Location for the type of location, and UFO and
Shot can then use moveY with 3 and 3 to move the respective objects:
class UFO {
class Shot {
...
...
// move this UFO down by 3 pixels // move this Shot up by 3 pixels
UFO move() {
Shot move() {
return
return
new UFO(this.location.moveY(3));
new Shot(this.location.moveY(3));
}
}
...
...
}
}
Figure 98 displays the class diagram with all the changes. In this diagram,
you can now see two places where the actual program extends a class from
the draw package: World and Posn. Study it well to understand how it
differs from the diagram in figure 93.
Exercises
Exercise 19.21 Define an interface that specifies the common methods of
the Shot and UFO classes.
Exercise 19.22 Collect all code fragments for the classes UFOWorld, UFO,
AUP, IShots, MtShots, and ConsShots and develop tests. Then introduce
AMovable; make sure the tests still work. Finally define Location; again
make sure the tests still work.
Exercise 19.23 Exercises 19.18 and 19.20 spice up the game with random
movements by the UFO and random counter-attacks. Create a union of
movable objects in this context, starting with UFO and Shot. Then turn the
class representing charges into a subclass of the AMovable class, too.
Compare the list of shots and the list of charges. Is there anything to
abstract here? Dont do it yet. Read chapter V first.
288
Section 19
#
#
#
# draw:
#
#
+------------------+ #
+--------+
#
| World
| # +-----------> | IShots |<-------------+
#
+------------------+ # |
+--------+
|
# +------+
| Canvas theCanvas | # |
|
|
# | Posn |
+------------------+ # |
/ \
|
# +------+
|
# |
--|
# | int x|
|
# |
|
|
# | int y|
|
# |
-----------------|
# +------+
|
# |
|
|
|
#
|
|
# |
+---------+
+-------------+
|
#
|
|
# |
| MtShots |
| ConsShots
|
|
#
|
|
# |
+---------+
+-------------+
|
#
/ \
/ \
# |
+---------+
| IShots rest |----+
#
-----# |
| Shot first |----+
#
|
|
# |
+-------------+
|
#
|
========================= |
|
==================
|
|
+---------------+ |
|
|
|
| AMovable
| |
|
|
|
+---------------+ |
|
+------------------+
|
| IColor color | |
+------------------+
| UFOWorld
|
|
| Location loc |-------->| Location
|
+------------------+
|
+---------------+ |
+------------------+
| int WIDTH
|
|
|
|
+------------------+
| int WIDTH
|
|
/ \
|
| Location moveY() |
| int WIDTH
|
|
--|
+------------------+
| int WIDTH
|
|
|
|
| int HEIGHT
|
|
+--------------------+
|
| IColor BACKG
|
|
|
|
|
| UFO ufo
|----|--+
|
|
v
| AUP aup
|----|-+|
+----------------+
+---------------+
| IShots shots
|----+ |+-->| UFO
|
| Shot
|
+------------------+
|
+----------------+
+---------------+
v
+----------------+
| AUP
|
+----------------+
| IColor aupColor|
| int location
|
+----------------+
draw:
into the wall; otherwise the game is over. Instead, use the arrow keys to
control the worms movements.
The goal of the game is to have the worm eat as much food as possible.
As the worm eats the food, it becomes longer; more and more segments
appear. Once a piece of food is digested, another piece appears. Of course,
the worms growth is dangerous. It can now run into itself and, if it does,
the game is over, too.
This sequence of screen shots illustrates how the game works in practice:
289
On the left, you see the initial setting. The worm consists of a single segment, its head. It is moving toward the food, which is a rectangular block.
The screen shot in the center shows the situation after some feedings. The
worm now has eight segments (plus the head) and is squiggling toward a
piece of food in the lower right corner. In the right-most screen shot the
worm has run into the right wall. The game is over; the player scored 11
points.
The following exercises guide you through the design and implementation of a Worm game. They follow the design recipe and use iterative
refinement. Feel free to create variations.
Exercises
Exercise 19.24 Design a WormWorld class that extends World. Equip the
class with onTick and onKeyEvent methods that do nothing. Design the
method draw, which should draw a yellow background for now.
Assume that neither the Food nor the Worm class has any attributes or
any properties. Draw a class diagram anyway; maintain it throughout the
exercise.
Make sure that the following example class works:
class Examples {
Worm w = new Worm();
Food f = new Food();
WormWorld ww = new WormWorld(this.f ,this.w);
boolean testRun = check this.ww.run() expect true; // keep last test
}
That is, design a method run that starts the clock, displays the canvas, and
invokes draw.
290
Section 19
291
next, which creates the next food particle and ensures that it is not at
the same spot as this one.
Hint: This design requires a modicum of generative recursion (see
How to Design Programs(Part V) or exercise 19.18).
Integrate it with the WormWorld from exercise 19.28, so that the display
shows the food. You do not need to worry about what happens when the
worm gets close to, or runs over, the food.
Exercise 19.30 Design the method eat for Worm from exercise 19.28. Eating
means that the head moves another step in the same direction in which the
worm was moving and the worms tail grows by one segment. Explain the
assumption behind this description of eating.
Integrate the modified Worm class with the WormWorld class from exercise 19.29. A worm should eat only if the food particle is eatable.
Exercise 19.31 Design the class Game, which sets up a worm game with a
random initial position for a piece of food and a worm with just a head,
and a method for starting the game.
Exercise 19.32 Edit your program, i.e., look for opportunities to abstract.
Section 20
292
easily checkable form. Back then we explained that this is what types are
all about. Types for fields and method signatures arent enough though. In
this section, we introduce three essential linguistic mechanisms from Java
for establishing invariants, for maintaining them, and for protecting them.
It represents dates via the common numeric notation, e.g., new Date(5, 6,
2003) stands for June 5, 2003. The problem is, however, that some other
part of the program canaccidentally or intentionallycreate an instance
like new Date(45, 77, 2003), which has no meaning in the real world.
The programmer has dutifully documented this assumption but the
code does not enforce them. By choosing int as the type for the fields, the
programmer ensures that the Date always consists of Java-style integers,
but this specification cannot even ensure that the given ints are positive
numbers. Thus, new Date(2,77,3000) is also an instance of the class.
In Professors Intermediate language we can address this problem with
a conditional constructor for the class: see figure 99. The constructor in
this revised Date class consists of an if statement. The test ensures that
the value of day is between 1 and 31, the value of month is between 1 and
12, and year is greater than 1900. If so, the constructor sets up the object
via initialization equations as usual; otherwise, it signals an error with
Util.error. Put differently, the constructor expresses the informal comments
of the original design via a test and thus guarantees that each instance of
Date satisfies the desired conditions.
Figure 100 shows a similar revision of the data representation of shapes,
illustrated with the case of circles. The inside-the-quadrant assumption for
shapes is attached to the interface. It is relevant, for example, for computing
293
class Date {
int day;
int month;
int year;
Section 20
294
boolean draw(Canvas c) {
boolean draw(Canvas c) {
return c.drawDisk(. . . )
return c.drawDisk(. . . )
295
Section 20
296
It turns out that the second solution isnt completely wrong, though this is
a topic for the next section and an improved language.
Exercises
Exercise 20.1 Refine the constructor for the Date class in figure 99 even
more. Specifically, ensure that the created Date uses a value of 31 for day
field only if the given month has that many days. Also rule out dates such
as new Date(30,2,2010), because the month of February never has 30 days
and therefore such a Date should not be created.
Hint: Remember that only January, March, May, July, August, October,
and December have 31 days.
If you are ambitious, research the rules that govern leap years and enforce the proper rules for 30-day months and February, too.
Exercise 20.2 Rectangles like circles are supposed to be located within the
quadrant that the canvas represents. Determine conditions that ensure that
a rectangle respects this constraint. Design a constructor that ensures that
the given initial values satisfy these conditions.
ProfessorJ:
. . . + access
If you change ProfessorJs language to Intermediate + access, you gain several new ways of expressing your thoughts on design.
The first and relevant one here is the power of OVERLOADING a constructor.41 To overload a constructor means to define several constructors,
each consuming different types of arguments. You can also overload methods in this manner. While this concept is also useful for methods, we explain it exclusively with constructors here. Overloading for methods works
in the exact same manner; we introduce it later when needed.
Figure 101 shows on the right side a version of DrpBlock that includes
two constructors:
1. the original one, which is needed for the drop method;
2. and the new one, discussed above, which creates blocks according to
the original problem statement.
41 Other
297
class DrpBlock {
int x;
int y;
int SIZE = 10;
class DrpBlock {
int x;
int y;
int SIZE = 10;
DrpBlock() {
this.x = 10;
this.y = 20;
}
DrpBlock(int x, int y) {
this.x = x;
this.y = y;
}
DrpBlock(int x, int y) {
this.x = x;
this.y = y;
}
DrpBlock drop() {
return
new DrpBlock(this.x,this.y+1);
}
DrpBlock drop() {
return
new DrpBlock(this.x,this.y+1) ;
}
class Example {
DrbBlock db1 = new DrpBlock() ;
1
Section 20
298
picks the one, matching constructor as the intended one for future program
evaluations.42
The example in figure 101 illustrates this point directly. The two constructors have distinct signatures. The first one consumes no arguments;
the second consumes two ints. Next take a close look at the three grayshaded constructor expressions in the figure. The one in the class itself use
two ints as arguments; hence it refers to the second constructor. The constructor expression with subscript 1 takes no arguments, meaning it is a
reference to the first constructor in the class. Finally, the expression with
subscript 2 takes two ints again and therefore uses the second constructor.
Sadly, the introduction of overloading doesnt solve the problem completely. We need even more expressive power. While code outside of DrpBlock can utilize the no-argument constructor and thus obtain an appropriate block in the desired initial state, nothing forces the use of this constructor. Indeed, as the figure shows the two-argument constructor is usable
and used in the Example class. In other words, the introduction of overloaded constructors opens possibilities for violating unspoken or informal
assumptions that we cannot just ignore if we wish to reason about code.
class DrpBlock {
private int x;
private int y;
private int SIZE = 10;
public DrpBlock() {
this.x = 10;
this.y = 20;
}
299
class ExampleBad {
DrbBlock db1 = new DrpBlock() ;
1
boolean test1 =
check this.db1.drop()
expect new DrpBlock(10,21) ;
Section 20
300
The left column in figure 102 illustrates how to use these adjectives to let
every reader and Java know that the argument-less constructor is useful for
everyonethe world outside the current class as well as the class itself
and that the second constructor is only useful for use within the class. If
some other class or the Interactions Window in ProfessorJ now contains
code such as new DrpBlock(30,40)), Java does not run the program.
With this in mind, take a look at the right column. It contains a sample
class for testing the only method of DrpBlock: drop. The class contains two
gray-shaded constructor expression with subscripts; one is legal now, the
other one is not. Specifically, while the expression with subscript 1 is legal because it uses the constructor without arguments, the expression with
subscript 2 is illegal given that the constructor of two arguments is private.
Thus, the EampleBad class does not type check, i.e., ProfessorJ highlights
the expression and explains that it is inaccessible outside of DrpBlock.
In addition, the code in the figure labels every field as private so that
other classes cant exploit the internal data representation of the field. Thus,
neither x nor y are accessible attributes outside the class. As a result, it is
impossible to write the following tests in a sample class:
check new DrpBlock().drop().x expect 10
&&
check new DrpBlock().drop().y expect 21
Put differently, the method in DrpBlock is not testable given the current
privacy specifications and method interface. In general, in the presence
of privacy specifications it occasionally becomes necessary to equip a class
with additional methods, simply so that you can test some existing classes.
Still, we can reason about this code and argue why it enforces the basic
assumption from the problem statement:
DrpBlock represents dropping blocks, which enter the world at
(10,20) and move straight down.
Because of the privacy adjectives, the first and public constructor is the only
way to create an instance of DrpBlock. This instance contains a block that is
at (10,20). An invocation of drop creates a block with the same x coordinate
and a y coordinate that is one larger than the one in the given block. Hence,
on a computer canvas, this new block would be drawn one pixel below the
given one. No other class can use the second constructor to create blocks at
random places, meaning we know that the described scenario is the only
one possible.
301
Exercises
Exercise 20.3 Design a method (or methods) so that you can test the drop
method in DrpBlock in the presence of the given privacy specifications.
Section 20
302
The third privacy adjective (protected) is useful for fields and methods in classes that serve as superclasses. Lets look at World from draw.ss,
which is intended as a superclass for different kinds of world-modeling
classes. Figure 104 displays a view of this World class with privacy specifications. As you can see, the class labels its canvas with protected, making it
visible to subclasses but inaccessible to others; after all, we dont want arbitrary methods to accidentally draw the wrong kind of shape on the canvas.
303
If an extension wishes to have some other class or method to have the canvas, then the methods in the subclasses must hand it out explicitly as an
argument to other classes. For an example, look at the draw in BlockWorld
(see figure 91); it uses this.theCanvas as an argument when it calls the draw
method of DrpBlock. When a method hands out the value of a protected
field, its programmer assumes responsibility (to the community of all programmers on this project) for the integrity of the associated assumptions.
+---------------------------------+ # +-------------------------------+
| World
| # | Block
|
+---------------------------------+ # +-------------------------------+
| protected Canvas theCanvas
| # | protected int down
|
+---------------------------------+ # | protected int right
|
| public World onTick()
| # +-------------------------------+
| public World onKeyEvent(String) | # | public boolean draw(Canvas)
|
| public boolean draw()
| # | private boolean paint(Canvas) |
| ...
| # +-------------------------------+
+---------------------------------+ #
|
|
#
|
|
#
|
|
#
|
/ \
#
/ \
--#
--|
#
|
======================================#
+-------+--- ...
|
|
+-----------------------------------+
|
| BlockWorld
|
|
+-----------------------------------+
+---------------------------+
| private DrpBlock block
|----->| DrpBlock
|
| private IColor BACKG
|
+---------------------------+
| public int WIDTH
|
| private int deltaY
|
| public int HEIGHT
|
+---------------------------+
+-----------------------------------+
| DrpBlock drop()
|
| public World onTick()
|
| DrpBlock steer(String ke) |
| public World onKeyEvent(String k) |
+---------------------------+
| public boolean draw()
|
| private boolean drawBackground() |
+-----------------------------------+
Section 20
304
305
public WormWorld() {
this.head = new Segment(this)
}
private Segment(Segment s) {
this.x = s.x;
this.y = s.y;
}
private WormWorld(Segment s) {
this.head = s;
}
public World onTick() { . . . }
Section 20
306
specifications.
Finally, modify the class so that it extends World and so that every
keystroke on the spacebar flips the switch.
Exercise 20.8 Take a look at the sketches of Select, Presentation, and Factory
in figure 107. Each refers to constructors in Item, the class on the left.
class Item {
int weight;
int price;
String quality;
public Item(int w,int p,String q) {
this.weight = w;
this.price = p;
this.quality = q;
}
public Item(int w,int p) {
this.weight = w;
this.price = p;
this.quality = "standard";
}
class Select {
. . . new Item(w,p,q) . . .
}
class Presentation {
int p;
...
boolean draw(String s) {
. . . new Item(p,s) . . .
}
...
}
class Factory {
...
int inquireInt(String s) { . . . }
Item create(. . . ) {
. . . new Item(
inquireInt("pounds"),
inquireInt("cents")) . . .
}
...
307
class Set {
private ILin elements;
public Set() {
this.elements = new MTLin();
}
private Set(ILin elements) {
this.elements = elements;
}
// add i to this set
// unless it is already in there
public Set add(int i) { . . . }
+------+
| ILin |<------------+
+------+
|
+------+
|
|
|
/ \
|
--|
|
|
---------------|
|
|
|
+-------+
+-----------+
|
| MTLin |
| Cin
|
|
+-------+
+-----------+
|
+-------+
| ILin more |----+
| int one
|
+-----------+
Argue that the following assumption holds, if add and in are completed
appropriately:
Lin does not contain any int twice.
Or show how to construct a set with the given constructors so that the assumption is wrong.
Complete the definition of Set with a public remove method.
Exercise 20.10 Design the class SortedList with the following interface:
interface ISortedList {
// add i to this list so that the result is sorted
ISortedList insert(int i);
// the first item on this list
int first();
// the remainder of this list
ISortedList rest();
}
The purpose of the class is to keep track of integers in an ascending manner:
check new SortedList().insert(3).insert(2).insert(4).first() expect 2
Section 21
308
What does
new SortedList().insert(3).insert(2).insert(4).rest().first()
produce? Make more examples!
Exercise 20.11 Re-visit the War of the Worlds project with encapsulation
in mind. Make sure that the UFO first appears at the top of the canvas,
that the AUP doesnt leave the canvas, that only those shots are in the list
of shots that are still within the UFOWorld. Can you think of additional
properties in this project that encapsulation can protect?
Exercise 20.12 Inspect your solution for the Worm game. Add privacy
specifications and ensure that you can still run all the tests.
309
inside of Coffee :
// is this the same Coffee as other?
boolean same(Coffee other)
The purpose statement phrases the problem as a direct question about this
object and the other instance of Coffee. After all, the idea of sameness is
about a comparison of two objects.
Given that Coffee has two fields, creating the template is also straightforward:
inside of Coffee :
// is this the same Coffee as other?
boolean same(Coffee other) {
. . . this.origin.mmm() . . . other.origin.mmm() . . .
. . . this.price . . . other.price . . .
}
It contains four expressions because two Coffee objects contain four fields.
The first two indicate that it is possible to invoke another method on the
value in the origin field because its type is String. From the template alone,
however, you cannot figure out what sameness could mean here. You need
examples, too:
Section 21
310
class EqExamples {
Coffee ethi = new Coffee("Ethiopian",1200);
Coffee kona = new Coffee("Kona",2095);
Coffee ethi1300 = (new Coffee("Ethiopian",1300));
boolean test1 = check this.ethi.same(this.ethi) expect true;
boolean test2 = check this.kona.same(this.ethi) expect false;
boolean test3 = check this.ethi.same(this.ethi1300) expect false;
}
The first example compares two instances of Coffee that have the exact same
attributes: both are from Ethiopia and both cost $12 per pound. Naturally,
you would expect true here. The second one compares two objects with
different origins and different prices. Unsurprisingly, the expected answer
is false. Last, even if two Coffees share the origin but have distinct prices,
they are distinct objects.
Together, the examples and the template suggest a point by pointthat
is, a field by fieldcomparison of the objects: see figure 109. For each kind
of value, you use the appropriate operation for comparisons. Here same
uses == for ints and equals for Strings. If you solved such exercises as 10.4
or recall the design of same for Cartesian points in chapter II, the definition
of same in Coffee is no surprise.
Given the presence of privacy specifications in the definition of Coffee,
you may wonder whether the field access in same works out as obviously
desired. Since these instructions for hiding the features of an object concern
the class, i.e., the program text, it turns out that one instance of Coffee can
access the secret, hidden fields of another instance of Coffee just fine.
Following philosophers, we call this notion of sameness EXTENSIONAL
EQUALITY .44 Roughly speaking, a method that implements extensional
equality compares two objects on a point by point basis. Usually, it just
compares the object one field at a time, using the equality notion that is
appropriate for the types of the fields. When you design an extensional
equality method, however, it is important to keep in mind what your class
represents and what this representation is to mean to an external observer.
Because this is what extensional equality is truly about: whatever you want
external observers to be able to compare.
A representation of mathematical sets via classes illustrates this point
well. To keep things simple, lets look at sets of two ints. Obviously, such
44 Frege (18481925)
311
Section 21
312
other.contains(this.one)
&& other.contains(this.two)
&& this.contains(other. one)
&& this.contains(other. two)
For the full definition of same for Set2, see figure 110.
A quick comparison shows that same in Set2 isnt just a field-by-field
comparison of the given objects. Instead, each field in this set is compared
to both fields of the other set. The success of one of these comparisons suffices. Thus, this notion of equality is significantly weaker than the field-byfield notion of equality for, say, Coffee. Here, it is the mathematics of sets
that tells you what sameness really means. In other cases, you will have to
work this out on your own. The question you will always need to ponder
is whether someone who doesnt know how you designed the class and
the method should be able to distinguish the two given objects. The answer will almost always depend on the circumstances, so our advice is to
explore the problem with as many examples as you need until you know
what others want from the class.
Exercises
Exercise 21.1 Develop data examples for Set2 and turn them into behavioral examples for contains and same. Translate them into a test suite and
run them. Be sure to include an example that requires all four lines in same.
Exercise 21.2 Design the method isSubset for Set2. The method determines
whether this instance of Set2 contains all the elements that some given instance of Set2 contains.
Mathematicians use s t to say that set s is a subset of set t. They
sometimes also use the notion of subset to define extensional set equality
as follows:
S1 = S2
means
( S1 S2 and
S2 S1)
313
Section 21
314
In this case, ProfessorJ would report one failed test, namely testD1. The
computed result is true while the expected result is false.
Of course, when you do compare a Coffee with an instance of Decaf , the
best you can do is compare them as Coffees. Conversely, a comparison of a
Decaf with a Coffee should work that way, too:
inside of InhExamples :
Coffee ethi = new Coffee("Ethiopian",1200);
boolean testCD = check this.ethi.same(this.decaf1) expect true;
boolean testDC = check this.ethi.same(this.decaf1) expect true;
Based on this analysis and the examples, you should realize that you
want (at least) two methods for comparing objects in Decaf:
inside of Decaf :
// is this Decaf the same as other when viewed as Coffee?
public boolean same(Coffee other)
// is this the same Decaf as other?
public boolean same(Decaf other)
The first is the inherited method. The second is the proper method for
comparing two instances of Decaf properly; we use the name same again
and thus overload the method. The type comparison that selects the proper
method thus decides with which method to compare coffees.
The method definition itself is straightforward:
inside of Decaf :
public boolean same(Decaf other) {
return super.same(other) && this.quality == other.quality;
}
It first invokes the super method, meaning the one that compares the two
instances as Coffees. Then it compares the two local fields to ensure that the
instances are truly the same as Decaf s.
In general, you will have to decide what a comparison means. In any
case, you will want to ensure that comparing objects is symmetric, that is,
no matter in what order you compare them, you get the same result. Keep
in mind, however, that your derived subclass always inherits the equality
method from its superclass so this form of comparison is always legal from
a type perspective. If you do not add an equality comparison for two instances of the subclass, you will get the inherited behavioreven if you do
not want it.
315
Exercises
Exercise 21.4 Design a complete examples class for comparing instances
of Coffee and Decaf . Evaluate the tests with the class definitions from figure 111 and 109. Then repeat the evaluation after adding the overloaded
same method to Decaf .
Section 21
316
to compare the int fields and same from Coffee to compare the Coffees (see the
gray-shaded box).
+-----------------------+
| IItem
|
+-----------------------+
+-----------------------+
| boolean same(IItem x) |
+-----------------------+
|
/ \
--|
-----------------------------|
|
+-----------------------+
+-----------------------+
| Coffee
|
| Tea
|
+-----------------------+
+-----------------------+
| String origin
|
| String kind
|
| int price
|
| int price
|
+-----------------------+
+-----------------------+
+-----------------------+
+-----------------------+
317
These two points explain the expected results of the following examples:
class ItemsExamples {
IItem ethi = new Coffee("Ethiopian",1200);
IItem blck = new Tea("Black",1200);
boolean test1 = check this.ethi.same(this.ethi) expect true;
boolean test2 = check this.ethi.same(this.blck) expect false;
boolean test3 = check this.blck.same(this.blck) expect true;
}
You can easily make up others; the idea should be clear.
The true nature of this design problem begins to show when you write
down the templates :
inside of Tea:
boolean same(IItem other) {
. . . this.kind . . . this.price . . .
. . . other.mmm() . . .
}
inside of Coffee:
boolean same(IItem other) {
. . . this.kind . . . this.price . . .
. . . other.mmm() . . .
}
Both classes contain two fields each, so the templates contain the two appropriate selector expressions. Since the arguments have a non-basic type,
the templates also contain a reminder that the methods can call an auxiliary
method on the argument.
Unfortunately, other itself has type IItem, which is an obstacle to making
progress. On one hand, we dont even know whether it makes sense to
compare this instance of Coffee, for example, with other; after all, other could
just be a Tea. On the other hand, even if we could validate that other is an
instance of Coffee, it is impossible to access its fields because the IItem type
doesnt provide any methods that access the fields of the implementing
classes. Indeed, thus far it only allows a method call to same or mmm, i.e.,
an auxiliary method.
What we really need then is two methods: one that checks whether an
IItem is an instance of Tea and another one that converts an IItem to a Tea, if
it is an instance of Tea. After the conversion, the same method can use the
familiar ways of comparing two Teas. Analogously, we need such methods
for Coffee, too:
Section 21
318
319
Exercises
Section 21
320
Exercise 21.5 Our chosen examples for the classes in figure 114 do not
cover all possible cases. Design additional test cases to do so.
Exercise 21.6 Add Chocolate as a third variant to the IItem union. The class
should keep track of two attributes: sweetness (a String) and price (an int).
What does it take to define its sameness method?
Exercise 21.7 Abstract over the commonalities of Coffee and Tea. Dont forget to use the tests from exercise 21.5. When you have completed this step,
repeat exercise 21.8. How does the abstraction facilitate the addition of new
variants? How is it still painful?
Exercise 21.8 Pick any self-referential datatype from chapter I and design
a same method for it. Remember the design recipe.
Exercise 21.9 On occasion, a union representation employs a string per
variant that uniquely identifies the class:
interface IMeasurement { }
class Meter implements IMeasurement {
private String name = "meter";
int x;
Meter(int x) {
this.x = x;
}
Add a variant to the union that measures distances in feet just like Meter
measures them in meters. Then add a method for comparing IMeasurements
that ignores the actual distance; in other words, it compares only the kind
of measurement not the value.
Similarly, a union may come with a method that converts an object to
a string. In that case, the comparison method can employ this method
and then compare the results. Equip IMeasurement with a toString method
whose purpose is to render any measurement as a string. Then add an
equality method to the union that compares complete measurements.
321
Todo
discrepancies between book and ProfessorJ: can we get overridden method
signatures with subtypes? is assignment still in Intermediate?
discuss overloading in this intermezzo
Intermezzo 3
322
323
PICTURE:
325
Intermezzo 3
326
TODO
add stacks and queues to the exercises (deck of cards looks like an ideal
place to do so)
the following works:
class Circular {
Circular one = this;
}
and creates a circular Object; I need to explain this
IV
When you go to a book store and ask a sales clerk to look up a book whose
author you remember but whose title you have forgotten, the clerk goes to
a computer, types in the name of the author, and retrieves the list of books
that the author has written. If you remember the title of the book but not
the author, the clerk enters the title of the book and retrieves the authors
name. Even though it is feasible for the program to maintain two copies of
all the information about books, it is much more natural to think of a data
representation in which books and authors directly refer to each other in a
circular manner.
So far, we havent seen anything like that. While pairs of data definitions
may refer to each other, their instances never do. That is, when an instance O
of class C refers to another object P, then P cannotdirectly or indirectly
refer back to O; of course, P may refer to other instances of C, but that is
not the same as referring to O. With what you have learned, you simply
cannot create such a collection of objects, as desirable as it may be. Bluntly
put, you cannot express the most natural representation of the book store
program.
The purpose of this chapter is to expand the expressive power of your
programming language. To this end, it introduces a new mechanism for
computing, specifically, the ability to change what a field represents. This is
called an assignment statement or assignment for short. Using assignments,
your methods can create pairs of objects that refer to each other. Your new
powers dont stop there, however. Once your methods can change the
value of a field, you also have another way of representing objects whose
state changes over time. Instead, of creating a new object when things
change, your methods can just change the values in fields. This second
idea is the topic of the second half of this chapter.
Section 23
328
+------------+
+--------------+
| Author
| <--+
+--> | Book
|
+------------+
|
|
+--------------+
| String fst |
|
|
| String title |
| String lst | +-|---+
| int price
|
| int dob
| | |
| int quantity |
| Book bk
|--+ +--------| Author ath
|
+------------+
+--------------+
// represent books
class Book {
String title;
int price;
int quantity;
Author ath;
23 Circular Data
Lets turn the problem of looking up books and authors into a programming scenario, based on the bookstore problem 11.1 (page 113):
. . . Design a program that assists bookstore employees. For
each book, the program keeps a record that includes information about its author, its title, its price, and its publication year.
In turn, the data representation for the author includes the authors first name, last name, year of birth, and the book written
by this author. . . .
The problem simplifies the real world a lot. Many authors write more than
one book. Similarly, many books have more than a single author; this book
has six, some have many more than that. Eventually we will have to associate an author with a list of books and a book with a list of authors.
Figure 115 displays a first-guess data representation, including a class
diagram and two class definitions. The class diagram differs from every-
Circular Data
329
thing we have seen so far in that it contains two classes, mutually connected
via containment arrows. Naturally, each of the two classes has four fields.
The Author class comes with four fields: a first name, a last name, an integer
that records the year of birth, and the book that the author wrote; the fields
in Book are for the title of the book, the sales price, the number of books in
the store, and the author of the book.
The next step in our design recipe calls for an exploration of examples.
Here, doing so proves again the value of a systematic approach. Consider
the following concrete example, a classic book in our discipline:
Donald E. Knuth. The Art of Computer Programming. Volume 1.
Addison Wesley, Reading, Massachusetts. 1968.
If we were to use the data representation of figure 115 and start with the
author, we easily get this far:
new Author("Donald",
"Knuth",
1938,
new Book("The Art of Computer Programming (volume 1)",
100,
2,
??? ))
Now the ??? should be replaced with the Author, but of course, that means
we would be starting all over again and there would obviously be no end
to this process. If we start with the book, we dont get any further either:
new Book("The Art of Computer Programming",
100,
2,
new Author("Donald",
"Knuth",
1938,
??? ))
In this case, the ??? should be replaced with a representation of the book
and that leads to an infinite process, too. At first glance, we are stuck.
Fortunately, we are not confronted with a chicken-and-egg problem; before authors are born, they cant write books. This suggests that an Author
should be created first and, when the book is created later, the program
should connect the instance of the given Author with the instance of the
Book. Figure 116 displays new versions of Author and Book that work in
Section 23
330
// represent books
class Book {
String title;
int price;
int quantity;
Author ath;
the suggested manner. A comparison with figure 115 shows two distinct
differences (gray-shaded):
1. The bk field in Author initially stands for null, an object that we havent
seen yet. In some way, null is unlike any other object. Its most distinguishing attribute is that it has all class and interface types. Hence,
any variable with such a type can stand for null. Think of null as a
wildcard value for now.
ProfessorJ:
Advanced
2. More surprisingly, the constructor for Book consists of five equations even though the class contains only four fields. The last one
is the new one. Its left-hand side refers to the bk field in the given ath;
the right-hand side is this, i.e., the instance of Book that is just being
created.
This situation is unusual because it is the very first time two equations in one programthe gray-shaded ones refer to the same field
in the same object. The implication is that equations in Java arent really mathematical equations. They are instructions to evaluate the righthand side and to change the meaning of the field (variable) on the
left-hand side: from now on the field stands for the value from the
right-hand sideuntil the next such instruction (with the same field
Circular Data
331
before:
auth
book
fst:
last:
"Donald"
title:
"Knuth"
price:
"TAOCP"
100
dob:
1938
quantity:
bk:
null
ath:
dob:
bk:
this.ath.bk = this
after:
auth
book
fst:
"Donald"
title:
"TAOCP"
last:
"Knuth"
price:
100
1938
quantity:
ath:
Creating instances of these revised classes looks just like before. The
constructor of the class is called with as many values as needed, and its
Section 23
332
Circular Data
333
// represent books
class Book {
String title;
int price;
int quantity;
Author ath;
this.bk = bk;
return ;
this.ath.addBook(this) ;
bottom of the constructor. These calls typically involve this, the newly created and previously unavailable object. Other objects may have to know
the new object (this) and informing them about it during the construction
of this is the most opportune time.
Figure 118 demonstrates this point with a modification of the running
example. The revised constructor ends in a call to the method addBook of
ath, the given author. This new method has a novel return type: void. This
type tells any future reader that the methods purpose is to change the field
values of the object and nothing else. To signal the success of the operation,
such methods return a single value, which has no other significance and is
therefore invisible.
The body of addBook also has a novel shape for methods, though it looks
almost like a simple constructor. Like a constructor, it consists of an assignment statement separated from the rest via a semicolon (;). Unlike a constructor, the method body ends with a return ; statement; it reminds the
reader again that the method produces the single, invisible void value and
Section 23
334
people omit such empty return statements; we believe it helps readers and
therefore use it.
Circular Data
335
interface is the type of all lists of books. Thus, Author doesnt directly refer
to Book but indirectly, through ConsBook and its first field. Of course, Book
still refers to Author. Otherwise this class diagram doesnt differ much from
those we have seen plus the new element of a cycle of containment arrows.
Translating the class diagram of figure 119 into class definitions poses
the same problem as translating the class diagram at the top of figure 115
into code. Like then, lets agree that the constructor for Author doesnt consume a list of books. Instead, the books field is initially set to new MTBooks(),
the empty list of books. As books are added to the collection of data, the
list must be updated.
// authors of books
class Author {
String fst;
String lst;
int dob;
IBooks books = new MTBooks();
Author(String fst, String lst,
int dob) {
this.fst = fst;
this.lst = lst;
this.dob = dob;
}
// lists of books
interface IBooks { }
class MTBooks
implements IBooks {
MTBooks() {}
}
class ConsBooks
implements IBooks {
Book fst;
IBooks rst;
ConsBooks(Book fst,
IBooks rst) {
this.fst = fst;
this.rst = rst;
}
Section 23
336
Afterwards, it assigns this value to the books field in Author. In other words,
from now on the books field stands for the newly created list. The rest of
Book class remains the same.
Exercises
Exercise 23.1 Explain why the definition of Book remained the same when
we changed Author to be associated with an entire list of Books.
Exercise 23.2 Create data representations for the following list of classics
in computer science:
1. Donald E. Knuth. The Art of Computer Programming. Volume 1. Addison Wesley, Reading, Massachusetts. 1968.
2. Donald E. Knuth. The Art of Computer Programming. Volume 2. Addison Wesley, Reading, Massachusetts. 1969.
3. Donald E. Knuth. The Art of Computer Programming. Volume 3. Addison Wesley, Reading, Massachusetts. 1970.
Draw a box diagram like the one in figure 117 for this example.
Or, do it for these books:
1. Henk Barendregt. The Lambda Calculus. North Holland, Amsterdam,
The Netherlands. 1981.
2. Daniel P. Friedman. The Little LISPer. SRA Press, Chicago, Illinois.
1974
3. Guy L. Steele Jr. Common LISP, the Language. Digital Press, Bedford,
Massachusetts. 1990.
Computer scientists should know (about) them, too.
Exercise 23.3 Modify the data representation for authors and books in figure 120 so that a new book is added to the end of an authors list.
Exercise 23.4 Encapsulate the state of Book and Author for the code in figures 116 and 118.
Circular Data
337
Section 23
338
the empty list of books for this purpose. Sometimes we need to use
the value of last resort: null.
Fourth, define an add method that assigns new values to cf . For now,
use the examples from this section as templates; they either replace
the value of cf with a given value or create a list of values. You will
soon learn how to design such methods in general.
Last, modify the constructors of the classes that implement CT. They
must call the add method with this so that the circular references can
be established.
5. Lastly, translate the circular examples of information into data, using
just the constructors in the enforced order. Check whether the circular
references exist by looking at it.
Exercises
Exercise 23.5 Design a data representation for a hospitals doctors and patients. A patients record contains the first and last name; the patients gender; the blood type; and the assigned primary physician. A doctors record
should specify a first and last name, an emergency phone number (int), and
the assigned patients.
Exercise 23.6 Design a data representation for your registrars office. Information about a course includes a department name (string), a course number, an instructor, and an enrollment, which you should represent with a
list of students. For a student, the registrar keeps track of the first and last
name and the list of courses for which the student has signed up. For an
instructor, the registrar also keeps track of the first and last name as well as
a list of currently assigned courses.
Exercise 23.7 Many cities deploy information kiosks in subway stations to
help tourists choose the correct train. At any station on a subway line, a
tourist can enter the name of some destination; the kiosk responds with
directions on how to get there from here.
The goal of this exercise is to design and explore two different data representations of a straight-line subway line. One way to represent a line for
an information kiosk is as a list of train station. Each station comes with
two lists: the list of stops (names) from this one to one end of the line and
the stops from this one to the other end of the line. Another way is to think
Circular Data
339
of a subway stop as a name combined with two other stops: the next stop
in each direction. This second way is close to the physical arrangement; it is
also an example of a doubly linked list. Hint: Consider designing the method
connect for this second representation of a subway station. The purpose of
this method is to connect this station to its two neighbors. The end stations
dont have neighbors.
class StrangeExample {
int x;
StrangeExample(
x = 100,
test = false)
}
this -
x:
test:
???
this -
x:
test:
false
this.x = 100
the constructed object
this -
x:
test:
100
false
Design both representations. Represent the Boston red line, which consists of the following stops: JFK, Andrew, Broadway, South Station, Downtown Crossing, Park Street, MGH, Kendall, Central, Harvard, Porter, Davis,
and Alewife.
340
Section 23
Circular Data
341
+-------+
| IList |<----------+
+-------+
|
|
|
/ \
|
--|
|
|
+------+------+
|
|
|
|
+------+
+-----------+ |
| MT
|
| ConsLog | |
+------+
+-----------+ |
| int fst
| |
| IList rst |-+
+-----------+
interface IList {}
class MT implements IList {}
class Cons implements IList {
int fst;
IList rst;
class Example {
Cons alist = new Cons(1,new MT());
Example() {
this.alist.rst = alist;
}
It has a single field: alist, which has the initial value new Cons(1,new MT()).
The constructor then assigns the value of alist to the rst field of alist, which
makes it a circular list.
We can visualize Examples alist and the assignment to alist using the
box diagram for structures from How to Design Programs:
before:
alist -
fst:
rst:
new MT()
alist.rst = rst
after:
alist
fst:
rst:
The boxes have two compartments, because Cons has two fields. Initially
the content of the first compartment is 1; new MT() is in the second compartment. The box itself is labeled alist, which is its name. When the constructor assigns alist to rst of alist, it sticks the entire box into itself. We
Section 23
342
cant draw such a box, because it would take us forever; but we can indicate this self-containment with an arrow that goes from the inside of the
compartment back to the box, the exact same place that is called alist.
Exercise
Exercise 23.8 Design the method length for IList. The method counts how
many ints this IList contains. After you test the method on regular examples, run it on alist from Example on the previous page:
new Example().alist.length()
What do you observe? Why?
The example suggests that assignment statements not only add necessary power; they also make it easy to mess up. Until now we didnt need
circular lists, and there is no obvious reason that we should be able to create
them unless there is an explicit demand for it. Otherwise we can get truly
unwanted behavior that is difficult to explain and, when it shows up later
by accident, is even more difficult to find and eliminate.
Our solution is to use privacy declarations for all fields and for all methods as advertised at the end of the preceding part. In this case, we just
need to protect the two fields in Cons and publicize the constructor: see
figure 123. With this protection in place, it is possible to create instances of
Cons and impossible to assign new values to these fields from the outside.
Hence, it is also impossible to create circular lists.
Circular Data
343
Carl (1926)
Bettina (1926)
Eyes: green
Eyes: green
X
HX
HH
HXXX
HH XX
HH
XX
H
X
XXX HH
H
H
XXXHH
H
XXH
H
XX
H
H
Adam (1950)
Eyes: yellow
Dave (1955)
Eyes: black
Eva (1965)
Fred (1966)
Eyes: blue
Eyes: pink
@
@
@
@
@
Gustav (1988)
Eyes: brown
Section 23
344
A Parent is a structure:
(make-parent LOC String Number String)
A list-of-children (short: LOC) is either
(a) empty; or
(b) (cons Parent LOC)
To create the ancestor family tree of a newborn child, we call the constructor
with information about the child and representations of the mothers and
fathers family tree:
(define father . . . )
(define mother . . . )
(define child (make-child father mother "Matthew" 10 "blue"))
That is, the data for the ancestors must exist before child is created. In contrast, to create a descendant tree for a mother, we create a representation of
her family tree using the list of her children and other personal information:
(define loc (list . . . ))
(define mother (make-parent loc "Wen" 30 "green"))
Thus, in an ancestor tree, it is easy to find all ancestors of a given person in
the tree; in a descendant tree, it is easy to find all descendants.
Even if we didnt have any experience with family tree representations,
just replacing the lines in figure 124 with arrows from children to parents
Circular Data
345
would give us the same insight. Such arrows lead to containment arrows
in a class diagram, meaning a node in the family tree contains fields for
parents (father, mother). This, in turn, means a method can follow these arrows only in the ancestor direction. Conversely, if we point the arrows from
parents to children, the nodes contain lists of children, and the methods can
easily compute facts about descendants.
+-------------+<-----------------+
| IFamilyNode |<----------------+|
+-------------+
||
|
||
/ \
||
--||
|
||
+--------------------------------+
||
|
|
||
+-------------------+
+-------------------+ ||
+->| Person
|
| UnknownPerson
| ||
| +-------------------+
+-------------------+ ||
| | String name
|
| IPersons children | ||
| |
|
+-------------------+ ||
| |
|
||
| | FamilyNode father |---------------------------------+|
| | FamilyNode mother |----------------------------------+
| |
|
+----------+
| | IPersons children |------>| IPersons |<----------------+
| +-------------------+
+----------+
|
|
|
|
|
/ \
|
|
--|
|
|
|
|
+--+------------+
|
|
|
|
|
|
+-----------+ +--------------+ |
|
| MTPersons | | ConsPersons | |
|
+-----------+ +--------------+ |
|
|
| |
+------------------------------------------| Person fst
| |
| IPersons rst |-+
+--------------+
Section 23
346
Figure 125 translates this problem analysis into a class diagram. Most of
the class diagram is straightforward. The upper half is the class-based
equivalent of the FTN definition (ancestor family tree) above: the interface
IFamilyNode is the type and the two implementing classesPerson and UnknownPersonare the variants. The lower half is the class-based equivalent
of the Parent and LOC definitions above: Person corresponds to Parent and
IPersons corresponds to LOC.
class Unknown
implements IFamilyTree {
IPersons children = new MTPersons();
Unknown() {}
void addChild(Person child) {
this.children =
new ConsPerson(child, this.children);
// list of Persons
interface IPersons {}
class MTPersons
implements IPersons {
MTPersons() {}
}
class ConsPerson
implements IPersons {
Person fst;
}
IPersons rst;
ConsPerson(Person fst, IPersons rst) {
void addChild(Person child) {
this.fst = fst;
this.children =
this.rst = rst;
new ConsPerson(child, this.children); }
}
}
this.mother.addChild(this);
this.father.addChild(this);
Circular Data
347
Section 24
348
list of children. The last step in the design recipe is to ensure that we can
represent information examples properly, including the circular ones. We
leave this to an exercise.
Exercises
Exercise 23.9 Use the class definitions in figure 126 to represent the information in figure 124 as data. Ensure that you can navigate from one sibling
to another via the existing methods and fields. Draw a box diagram for the
resulting objects.
Exercise 23.10 Create an instance of Person whose parent fields point to
itself. Add privacy declarations to the classes in figure 126 so that the state
of all objects is encapsulated and hidden from the rest of the program. Can
these declarations prevent self-parenting instances of Person?
Exercise 23.11 Abstract over the common patterns (see the Person and Unknown classes) in figure 126.
Exercise 23.12 Design a data representation for a file system like the one
found on your computer. Make sure you can implement methods for listing
all files and folders in a folder; for changing the focus to a folder that is
inside the current folder; and for changing the focus to the parent of the
current folder. Which of the potential design tasks, if any, require you to
design circular relationships into the data representation?
Exercise 23.13 Design a data representation for a representing a river system. You must be able to go back and forth along the river, including from
the confluence where a tributary joins to both of the preceding points. See
section 5.2 for a simple representation of river systems.
349
+----------+
| IAuthors |<------------------+
+------------+
+--------------+
+----------+
|
+--->| Author
|<---+
+-->| Book
|
|
|
|
+------------+
|
|
+--------------+
/ \
|
|
| String fst |
|
|
| String title |
--|
|
| String lst | +--/2/-+
| int price
|
|
|
|
| int dob
| | |
| int quantity |
+--+------------+
|
|
| Book bk
|--+ +--/1/---| Author ath
|
|
|
|
|
+------------+
+--------------+
+-----------+ +--------------+ |
|
| MTAuthors | | ConsAuthors | |
|
+-----------+ +--------------+ |
|
| Author fst
|-|----+
| IAuthors rst |-+
+--------------+
inside of ConsAuthors :
String lookupTitle(String last) {
. . . this.fst.lookupTitle(last) . . .
. . . this.rst.lookupTitle(last) . . .
}
Section 24
350
inside of Author :
String lookupTitle(String last) {
. . . this.fst . . . // String
. . . this.lst . . . // String
. . . this.dob . . . // int
. . . this.bk.lookupTitle(last) . . .
}
Exercises
Exercise 24.1 Figure 128 displays the portion of the program that deals
with a list of authors. In the bottom left corner, the figure displays a oneitem wish list. Modify the class definitions of Author and Book from figure 116 or 118 to work with the fragment of figure 128.
Exercise 24.2 The lookupTitle method returns "" if the list doesnt contain
an instance of Author with the given last name. While this trick is possibly
justifiable for the lookupTitle method in IAuthors, it does not produce welldesigned programs in general.
At this point it is important to remember that the design recipe for
methods does not demand a method of the same name in a contained class.
It just suggests that you might need some auxiliary method (or several) in
351
interface IAuthors {
// retrieve the title of lasts book from this author list
// return "" if no author with this last name exists
String lookupTitle(String last);
}
class MTAuthors
implements IAuthors {
MTAuthors() {}
class ConsAuthors
implements IAuthors {
Author fst;
IAuthors rst;
wish list:
inside of Author :
// return title of the book if
// this author has the name last;
// return "" otherwise
String lookupTitle(String last)
the contained class. Use this reminder to formulate a more general template for ConsAuthors, Author, and Book. Then define a variant of the program that separates the task of checking for the authors last name from the
task of retrieving the title of the authors book.
Exercise 24.3 Add a data representation for lists of Books to figure 127. Design the classes and a method for looking up the last name of an author by
the book title. Also design a method for looking up how many copies of a
given book title the store has.
Its time to turn to the full-fledged bookstore problem where books have
many authors and authors write many books. The diagram in figure 119
(page 334) solves half the problem; there, each author is associated with
many books though each book has exactly one author. To attach many
authors to one book, we need an appropriate field in Book:
Section 24
352
IAuthors authors;
where IAuthors is the type of a list of Authors.
Figure 129 shows the suggested revision of figure 119. Two of the arrows are labeled: 1 labels the containment arrow from Book to IAuthors; 2
is attached to the corresponding arrow from Author to IBooks. Each of the
two arrows can be used to create a cycle in a collection of instances of these
classes. Try it out!
The translation of the class diagram in figure 129 into class definitions
can start from the classes in figure 120. In this figure, the Author class provides a method for adding a book to an authors list of books; the constructor of the Book class calls this method every time a book is created for that
author, thus adding the book itself to the list. This invocation of addBook
must change, however; see figure 130. After all, a Book doesnt have a single author anymore, it has many. And adding the book to all authors book
lists requires a method by itself.
Thus our attempt to translate a data description into class definitions
has run into a surprising obstacle:
defining the classes requires the design of a non-trivial method.
In other words, data design and method design can now depend on each
other: to define the Book class, we need a method that adds the book to each
author, and to define this method we really need the class definitions.
Fortunately we know how to design methods, at least in principle, so
lets forge ahead. Specifically, the design of methods can proceed based on
the class diagram, and this is what we try here. Following the precedent
from the first example in this section, we ignore the containment arrow
pointing back from Author to Booklabeled 2 in figure 129for the design
of the method. Without this arrow, the design problem is just another instance of the problems we know from chapter II.
Representing a list of authors is easy at this point; we use IAuthors for
the type of lists, and MtAuthors and ConsAuthors for the concrete variants.
The method signature of addBookToAll and purpose statement shows up in
the interface:
inside of IAuthors :
// add the book to all the authors on this list
void addBookToAll(Book bk);
The return type is void because the purpose of the method is modify each
item on the list, not to compute something. The appropriate method templates look like this:
353
+----------+
+----------+
| IBooks
|<------------------+
+--->| IAuthors |<------------------+
+----------+<----------------+ |
|
+----------+
|
|
| |
|
|
|
/ \
| |
|
/ \
|
--| |
|
--|
|
| |
|
|
|
+--+------------+
| |
|
+--+------------+
|
|
|
| |
|
|
|
|
+---------+ +--------------+ | |
|
+-----------+ +--------------+ |
| MTBooks | | ConsBooks
| | |
|
| MTAuthors | | ConsAuthors | |
+---------+ +--------------+ | |
|
+-----------+ +--------------+ |
+----| Book fst
| | |
|
+-----| Author fst
| |
|
| IBooks rst
|-+ |
|
|
| IAuthors rst |-+
|
+--------------+
|
|
|
+--------------+
|
|
|
|
|
|
|
|
|
|
|
|
v
|
|
v
+------------------+
|
|
+----------------+
| Book
|
|
|
| Author
|
+------------------+
|
|
+----------------+
| String title
|
|
|
| String first
|
| IAuthors authors |--/1/-------------+
| String last
|
| int price
|
|
| int dob
|
| int quantity
|
+----/2/----| IBooks books
|
+------------------+
+----------------+
class Book {
String title;
int price;
int quantity;
IAuthors ath;
this.ath.???;
inside of MtAuthors :
void addBookToAll(Book b) {
...
}
inside of ConsAuthors :
void addBookToAll(Book b) {
. . . this.fst.addBook(b) . . .
. . . this.rst.addBookToAll(b) . . .
}
Section 24
354
// lists of authors
interface IAuthors {
// add the book to all the authors on this list
void addBookToAll(Book bk);
}
class MTAuthors
implements IAuthors {
MTAuthors() {}
class ConsAuthors
implements IAuthors {
Author fst;
IAuthors rst;
return ;
this.fst.addBook(bk);
this.rst.addBookToAll(bk);
return ;
Exercise
Exercise 24.4 Perform the last step of the design recipe for classes, which in
355
this case is also the first step of the design recipe for methods: the creation
of data examples. Represent the following information about authors and
books with our chosen classes:
1. Donald E. Knuth. The Art of Computer Programming. Volume 1. Addison Wesley, Reading, Massachusetts. 1968.
2. Donald E. Knuth. The Art of Computer Programming. Volume 2. Addison Wesley, Reading, Massachusetts. 1969.
3. Donald E. Knuth. The Art of Computer Programming. Volume 3. Addison Wesley, Reading, Massachusetts. 1970.
4. Matthias Felleisen, Robert B. Findler, Matthew Flatt, Shriram Krishnamurthi. How to Design Programs. MIT Press, Cambridge, Massachusetts. 2001.
5. Daniel P. Friedman, Matthias Felleisen. The Little LISPer. Trade Edition. MIT Press, Cambridge, Massachusetts. 1987.
Use auxiliary fields to make your life as easy as possible.
Now that we have a full-fledged data representation for books and authors, lets repeat the design of the lookupTitle method. Naturally, this time
the method doesnt return a single title but a list of titles, presumably represented as Strings. Lets use ITitles for the type of all lists of book titles;
you can define this representation yourself.
The first step is to write down the signature and purpose statement of
the method that goes into the interface for lists of authors:
inside of IAuthors :
// produce the list of book titles that
// the author wrote according to this list
ITitles lookupTitles(String last);
The method signature of lookupTitles is the translation of the problem statement into code: the method works on lists of authors and it consumes an
authors name. The result has type ITitles. This time we ignore link 1 in
figure 129, the containment arrow from Book back to IAuthors.
The second design step is to develop functional examples. For simplicity, we use the data examples from exercise 24.4, assuming you have
translated the information into data and called the resulting list all:
Section 24
356
inside of ConsAuthors :
ITitles lookupTitles(String lst) {
. . . this.fst . . . // Author
. . . this.rst.lookupTitles() . . . // ITitles
}
In the case of the empty list, we know nothing else. For the case of a constructed list, we know that there is an author and, via the natural recursion,
another list of titles.
You can finish the definition of lookupTitles in MTAuthors trivially; if
there are no (more) authors, the result is the empty list of titles. The case
of ConsAuthors requires a bit of planning, however. First, we distinguish
two cases: the Author object is for the author with last name lst or it isnt. In
the latter case, the result of the natural recursion is the result of the method.
Otherwise, the search process has found an author with the matching name
and therefore this.fst.bk provides access to the list of the authors books.
Put differently, we get two entries on our wish list:
1. is, a method that compares an authors last name with a given string:
inside of Author :
// is the last name of this author equal to lst?
boolean is(String lst)
2. allTitles, a method that extracts the list of titles from a list of books
inside of IBooks :
// the list of titles from this list of books
ITitles allTitles()
46 An
alternative is to represent the result as a set and to compare the results with setequality; see section 21.
357
The first method has a simple, one-line definition; the second one belongs
to lists of books and thus leads to a wish list entry.
Exercises
Exercise 24.5 Create an Example class from the test case for lookupTitles.
Develop two additional examples/tests: one for an author who has a single
book to his name and one for an author with no books.
Exercise 24.6 Complete the definition of lookupTitles assuming the methods
on the wish list exist.
The design of the allTitles method poses the same problem as the design
of lookupTitles. This time it is the authors field in Book that causes the circular
link in the chain. Hence we ignore this field and proceed as usual:
inside of IBooks :
// produce the titles of this list of books
ITitles allTitles();
The signature shows that the method just traverses an object of type IBooks,
i.e., a list of books; the purpose statement repeats our task statement in a
concise manner.
For the example step, if knuth stands for the list of all of Knuths books
from exercise 24.4 (in order), we should expect this result:
check knuth.bk.allTitles() expect
new ConsTitles("The Art of Computer Programming (volume 3)",
new ConsTitles("The Art of Computer Programming (volume 2)",
new ConsTitles("The Art of Computer Programming (volume 1)",
new MTTitles()))
This is, of course, also the result of all.lookupTitles("Knuth"). In other words,
we have exploited the example for one method to create an example for an
auxiliary method, which is a common way to develop tests.
Creating the template is also similar to what we did before:
inside of ConsBooks :
ITitles allTitles() {
. . . this.fst . . . // Book
. . . this.rst.allTitles() . . . // ITitles
}
As before, MTBooks contains no other fields, so the template contains no
inside of MTBooks :
ITitles allTitles() {
...
}
358
Section 24
expressions; the ConsBooks contains two fields and the method template
contains two lines: one that reminds of the first book and one that reminds
us of the natural recursion, which collects the rest of the list of titles.
The step from the template to the full definitions is again straightforward. One method returns the empty list of titles and the other one combines the title from the first book with the list of titles from the second book.
Exercises
Exercise 24.7 Design the is method for Author. Finish the design of allTitles;
in particular, run the test for the method and ensure it works.
Finally, after both definitions are confirmed to work run the tests from
exercise 24.5.
Exercise 24.8 Add computer science books of two distinct authors with the
same last name to the inventory. In this new context, develop a functional
example that uses this new last name; turn it into a test. If the program
from exercise 24.7 fails the test, modify it so that it passes this new test.
Exercise 24.9 Design ITitles so that it represents a set of titles instead of a
list. In other words, the order of entries shouldnt matter. Equip the class
with the method same, which checks whether this set contains the same
elements as some given set. Hint: See exercise 19.4 and Section 21.
Reformulate the test cases in exercise 24.5 using same and make sure that
your solution of exercise 24.7 still passes these tests. What is the advantage
of doing using sets?
Our simple idea worked. If we just ignore the fields that create circularity and proceed with the design of methods as before, we can successfully
design methods for circular data. In short, the problem is one of viewing the
data properly and of understanding the design of the data representation
from the perspective of the method design problem.
Exercises
Exercise 24.10 Design the method findAuthors, which given the title of a
book, produces the names of its authors.
Exercise 24.11 Add the following methods to the bookstore program:
359
1. titlesInYear, which produces a list of all book titles that some author
with given last name published during some given year;
2. allAuthors, which produces the list of all last names;
3. value, which computes the value of the current inventory.
Exercise 24.12 This exercise extends the design problem of exercise 23.6.
Design the following methods:
1. numberCourses, which counts the courses a student is taking;
2. takesCourse, which helps instructors figure out whether some student
(specified via a last name) is enrolled in a given course;
3. jointCourses, which allows the registrar to determine the enrollment
common to two distinct courses as a list of last names.
Also add enrollment limits to each course. Make sure that a method for
enrolling a student in this course enforces the enrollment limit.
Exercise 24.13 This exercise resumes exercise 23.5. Design the sameDoctor
method for the class of patients. The method consumes the last name of
some other patient and finds out whether the two are assigned to the same
primary care physician.
Exercise 24.14 Design the method findDirection to both data representations of a subway line from exercise 23.7. The method consumes the name
of the station to where a customer would like to go and produces a short
phrase, e.g., "take the train to ..." or "you are at ...".
Section 25
360
inside of Author :
this.books = new ConsBooks(bk,this.books);
or it is a field in some other class assuming this field is public, e.g.,
inside of Book :
this.ath.bk = this;
The purpose of an assignment statement is to change what the field
represents.
2. If the return type of a method is void, the method does not communicate the results of its computation directly to its caller. Instead, it
changes some fields, from which the caller or some other piece of
code retrieves them. When the method is done, it returns the invisible void value.
Your programs cant do much with the void value. It exists only as a
token that signals the end of some computation. Conversely, it allows
the next computation in the program to proceed.
3. The purpose of statement1 ; statement2 in Java programs is to say
that the evaluation of statement2 begins when statement1 is done. The
value of statement1 is ignored. In particular, when you see
this.field1 = field1;
this.field2 = field2;
...
in a constructor, the assignments are evaluated in this order, and their
results are thrown away.
All of this has profound implications. Most importantly, now time matters during program evaluation. In the past, we acted as if a field in a
specific object stands for one and the same value during the entire program evaluation. After the introduction of assignment statements, this is
no longer true; the value of a field can change and we must learn to figure out what the current value of a field is when we encounter a reference
during an evaluation.
Next, if assignments change what fields represent, we can use assignments to represent changes in the world of our programs. Consider a Java
object that represents a dropping ball. Thus far we have created a new object for every tick of the clock, but the fact that field values can be changed
suggests we have an alternative.
361
Last but not least, assignments change what we mean with equality.
When we update the list of books in an Author object, the object changes
yet it remains the same. When we propose to keep track of the height of a
dropping ball via assignment, we are proposing that the ball changes and
yet it also remains the same.
The remaining sections explore these topics in detail. First, we show
how to use our new powers to represent change. Second, we study equality,
i.e., what it means for two objects to be the same.
class DrpBlock {
int ow = 10;
int oh = 10;
int x;
int y;
IColor oc = new White();
class DrpBlock {
int ow = 10;
int oh = 10;
int x;
int y;
IColor oc = new White();
DrpBlock(int x, int y) {
this.x = x;
this.y = y;
}
DrpBlock(int x,int y) {
this.x = x;
this.y = y;
}
DrpBlock drop() {
return
new DrpBlock(this.x,this.y+1);
}
void drop() {
this.y = this.y + 1;
return ;
boolean isAt(int h) {
boolean isAt(int h) {
boolean draw(Canvas c) {
boolean draw(Canvas c) {
return c.drawRect(. . . );
return c.drawRect(. . . );
Section 26
362
pare and contrast this new idea with the old approach via three examples,
including a re-designed drawing package. These examples set up the next
section, which presents the principles of design of imperative classes and
methods.
class BlockWorld extends World {
private int WIDTH = 200;
private int HEIGHT = 200;
private IColor bgrdColor = new Red();
private DrpBlock block = new DrpBlock(this.WIDTH / 2,0);
public BlockWorld() {
this.bigBang(this.WIDTH,this.HEIGHT,.01);
}
public World onTick() {
this.block.drop();
if (this.block.isAt(this.HEIGHT)) {
return endOfWorld("stop!");
}
else {
return this; }
}
...
363
One way of viewing the two classes is to perceive them as two different
representations of change in the world (as in, the domain with which our
program acts). In the left version the current world, dubbed APPLICATIVE,
is a function of time; as time goes by, the program creates new worlds, each
representing another moment. This approach is typically used in physical models, chemistry, and other natural sciences as well as in engineering disciplines such as signal processing. In the right version, the world
stays the same but some of its attributes change. This second approach,
dubbed IMPERATIVE or STATEFUL in this book, is the electrical computer
engineers view of a changing world, based on the idea of computing as
turning switches on and off.47
Exercise
Exercise 26.1 Add privacy modifiers to the imperative DrpBlock class.
contrasts with a computer scientist who should be able to use and easily switch
between the two views.
Section 26
364
and is therefore the same as the old instance of BlockWorld. Therefore the
method might as well just produce this:
inside of BlockWorld : (imperative: version 2)
World onTick() {
this.block.drop();
return this;
}
Of course, we really want the block to land when its bottom reaches the
bottom of the canvas, which means that the actual method also needs to
perform some tests.
Figure 133 displays the final definition of onTick for the imperative version of DrpBlock: see the nested box. As discussed, the method first drops
the block. Then it uses the isAt method in DrpBlock to determine whether
block has reached the specified HEIGHT. If so, the world ends with the block
resting on the ground; otherwise, onTick returns this world.
365
Exercises
Exercise 26.2 Add privacy specifications to the applicative Account class.
Exercise 26.3 Add conditionals to Accounts constructor, deposit, and withdraw methods that check on the above stated, informal assumptions about
their int arguments. If any of the given amounts are out of the specified
range, use Util.error to signal an error.
To convert this class into an imperative one, lets follow the outline of
the preceding subsection. It suggests that the methods whose return type
is Account should be converted into methods that produce void. Instead of
creating a new instance of Account, the revised methods change the amount
field in the existing object. Specifically, deposit uses this assignment:
this.amount = this.amount + a;
48 This
Section 26
366
class Account {
int amount;
String holder;
class Account {
int amount;
String holder;
// a balance statement
// of this account
String balance() {
return
this.holder.
concat(": ".
concat(
String.valueOf (this.amount)));
}
// a balance statement
// of this account
String balance() {
return
this.holder.
concat(": ".
concat(
String.valueOf (this.amount)));
}
367
For the applicative version, we use a series of definitions. Each of them introduces a new instance (a1, a2) as the result of using deposit or withdraw on
the latest account. For the imperative one, we use a series of method invocations. Each of these invocations returns void after modifying the amount
field of a3. Because we dont have to care about the return values, we can
use ; to compose these statements. The significant difference between
the two examples is that the old accounts with the old balances are available in the applicative version; the assignments to amount in the imperative
versions literally destroy the relationship between amount and its values.
Section 26
368
creates a 200 by 200 canvas with a red background and the words hello
world close to the center.
// to represent a world
abstract class World {
Canvas theCanvas = new Canvas();
// open a width by height canvas,
// start the clock, and
// make this world the current one
void bigBang(int width, int height,
double s)
// process a keystroke
// event in this world
abstract void onKeyEvent(String ke)
// draw s at position p
void drawString(Posn p, String s)
...
// stop this worlds clock
World endOfTime()
Even though the use of boolean works reasonably well, the use of void
as a return type would express our intentions better than boolean. After
all, the methods just need to return anything to signal that they are done;
what they return doesnt matter. Furthermore, they also effect the state of
some object, in this case, the canvas on the computer screen. It is exactly
for this situation that Java provides void. Similarly, using && for sequencing changes to the canvas is a bit of an abuse of what boolean values are
all about. Once the methods return void, it becomes natural to use ; for
sequencing effects, and this is what it symbolizes. For example, the above
369
Section 26
370
So we start from scratch. The library design recipe says that we have to
design the four required50 methods:
import idraw.;
import colors.;
import geometry.;
class BlockWorld extends World {
...
private DrpBlock block;
public BlockWorld() { . . . to be designed . . . }
// draw the falling block in this world
public void draw() { . . . to be designed . . . }
// drop the block in this world by a pixel
public void onTick() { . . . to be designed . . . }
// do nothing
public void onKeyEvent(String ke) {
return ;
}
The block field is necessary because this world of blocks contains one dropping block; the onKeyEvent method doesnt do anything for now, so it just
returns void. Clearly, it is onTick (again) that requires our attention.
A brief consideration suggests that the task of onTick is to change the
block field; after all it is the only thing that changes in this world. We record
this insight in the purpose statement:
inside of BlockWorld :
// drop the block in this world by a pixel
// effect: change the block field
public void onTick() { . . . to be designed . . . }
This second line of the purpose statement is called an EFFECT STATEMENT.
The objective of an effect statement is to help us design the method and to
alert future readers to the change of fields that this method causes.
50 Recall
that labeling a method with abstract means that subclasses are required to implement them.
371
Together the purpose statement and the effect statement help us take
the first step in our design:
inside of BlockWorld :
// drop the block in this world by a pixel
// effect: change the block field
public void onTick() {
this.block = . . . a new block that is lower than the current one . . .
return ;
}
The mostly complete class definition for an imperative BlockWorld (using idraw) is shown on the left side of figure 136. The draw method is as
expected. It uses the drawing methods from Canvas and return void. The
onTick method performs one additional task compared to the one we design: when the block reaches HEIGHT, the clock is stopped and with it the
animation.
The right side of the figure defines a mostly applicative DrpBlock. Its
drop method is applicative, i.e., it returns a new instance of the class with
new coordinates. Indeed, the class has barely changed compared to its
purely applicative method. Only its draw method uses an imperative drawing method and ; to sequence two statements.
Exercise
Exercise 26.4 Design methods for controlling the descent of the block with
left and right arrow keys.
At this point, you may wonder what it would be like to use the imperative version of DrpBlock from figure 133. Even though the method signa-
Section 26
372
tures arent quite right, it is after all the block whose properties change over
time. Hence it should be designed using imperative methods and the rest
of the design should follow from this decision.
// an imperative world
class BlockWorld extends World {
private int HEIGHT = 200;
private int WIDTH = 200;
private IColor bgrdColor = new Red();
private DrpBlock block =
new DrpBlock(this.WIDTH/2,0);
// an applicative block
class DrpBlock {
int ow = 10;
int oh = 10;
int x;
int y;
IColor oc = new White();
DrpBlock(int x, int y) {
this.x = x;
this.y = y;
}
public BlockWorld() {
this.bigBang(. . . );
}
public void draw() {
this.block.draw(theCanvas);
return ;
}
DrpBlock drop() {
return
new DrpBlock(this.x,this.y+1);
}
boolean isAt(int h) {
}
boolean draw(Canvas c) {
c.drawRect(. . . );
return true;
373
Exercise
Exercise 26.5 Design methods for controlling the descent of the block with
left and right arrow keys for the classes of figure 137.
Section 27
374
// an imperative world
class BlockWorld extends World {
private int HEIGHT = 200;
private int WIDTH = 200;
private IColor bgrdColor = new Red();
private DrpBlock block =
new DrpBlock(this.WIDTH/2,0);
// an imperative block
class DrpBlock {
int ow = 10;
int oh = 10;
int x;
int y;
IColor oc = new White();
DrpBlock(int x, int y) {
this.x = x;
this.y = y;
}
public BlockWorld() {
this.bigBang(. . . );
}
public void draw() {
this.block.draw(theCanvas);
return ;
}
return ;
if (this.block.isAt(this.HEIGHT)) {
this.endOfTime();
return ;
}
else {
return ; }
boolean isAt(int h) {
}
void draw(Canvas c) {
c.drawRect(. . . );
return ;
Figure 137: The Very Last World of Dropping Blocks (idraw package)
375
library criterion(1)
library criterion(2)
that you have co-designed an imperative block world with an applicative block
inside (figure 136), you understand in principle how World itself works.
376
Section 27
In short, it is all about the line that somebody drew between your code and
the rest of the code, and this line determines the nature of your design.
While both version of the criterion are pragmatic and straightforward
to understand, they actually fail to answer the true question, which is when
we want to use stateful classes. After all, we still dont know why the designers of the library or the project chose to introduce stateful classes in the
first place. And if we wish to become good designers, we must understand
their reasoning, too.
So, lets ignore the library example from section 27 for a moment and focus on the first two: the stateful definition of DrpBlock and the Account class.
In both cases, we started from the premise that the class represents information about objects that change over time. One way to recognize whether
a class represents changing objects is to study the relationship among its
methods, before you implement them. If one of the methods should produce different results depending on whether or how the methods of the class
have been called in the past, consider using assignments to the fields. Put
differently, if time matters, applicative classes represent it explicitly; stateful classes represent it implicitly with assignment statements.
Consider the Account class and the methods that its graphical interface
suggests: deposit, withdraw, and balance. A moments thought suggests that
the result of balance should depend on what amounts the account holder
has deposited and withdrawn since the account has been created. Similarly,
for DrpBlock objects the result of isAt depends on the flight time of the block,
i.e., on how often drop has been called.
Thus, our second criteria is this:
If any of the to-be-designed methods of a
class must compute results that depend on
the history of method invocations (for an object), consider making the class stateful and
some of its methods imperative.
377
Since you know that alternatives exist, i.e., that it is possible to represent change with applicative classes and methods, you might wonder why
it is more likely that library designers use imperative approaches,52 thus
inducing you to use imperative methods. There are two different answers
to this question:
1. The first answer concerns the complexity of the software. Although
the stateful classes and imperative methods we discussed dont look
any simpler than their applicative counterparts, this is not true in general. The hallmark of the applicative approach is that a method signature describes all possible channels on which values may flow into
or out of a method. Of course, this is also a restriction and, as section 27.6 below shows, can lead to contorted signatures. In the worst
case, these contortions can become rather complex, though people
have not found a good way to measure this form of complexity.
2. The second answer concerns the complexity of the computation that
the software performs, which are measurable quantities53 such as
time, energy, or space. To this day, people dont know yet how to
make applicative classes and methods as efficient as imperative ones.
That is, for many situations an imperative approach to state-changes
over time run faster than applicative ones, consume less energy than
an applicative one, and so on.
How to Design Programs introduced the issue of running time only at
the very end. As you learn to design pieces of a program that others
may use or that are going to survive for a long time, you will need
to pay more and more attention to this issue. This chapter is a first
step in this direction. As you pursue this path keep in mind, though,
that before you worry about your methods running time or energy
consumption, you must get them correct and you must design them
so that you and others can understand and adapt them to new circumstances and improve them as needed.
With this in mind, lets turn to the revision of the design recipe.
52 This
is true for Java and many object-oriented programming languages, though not for
OCAML and PLT Scheme, which both support class systems, too.
53 They have therefore gained dominance in traditional approaches to programming.
378
Section 27
379
increasing the complexity of your world, you are almost always better off with an imperative design.
Note 2: The decision to make classes stateful may actually require a
change to your data definitions. This is especially true if you decide
that a class ought to come with imperative methods and your part of
the program doesnt have control over all the instances of the class.
Section 27.8 illustrates this point.
Hint: If the collection of classes under your control has a World-like
class that contains everything else, start with it. The verbs in the problem statement may help you figure out what kind of actions can be
performed on the objects of this class. Then, using your imagination
and experience, proceed along the containment and inheritance arrows to add methods in other classes that have to support those in
your all-encompassing one.
class definitions Define the interfaces and classes; equip each definition
with a purpose statement that explains its purpose. You may also
want to add a note saying to which of the fields methods may assign
new values.
data examples Translate examples of information from the problem statement into data examples. Also be sure that you can interpret data
examples in the problem domain. Provide examples that illustrate
how objects evolve over time.
With this new step in mind, let us take a look at the Account example again. When you are asked to design such a class, it is quite obvious
that there is one class with (at least) two fields (owner, amount) and three
methods: deposit, withdraw, and balance. We have already discussed how
the latters result depends on the invocation of the former methods. From
this we concluded that the class should be stateful, and it is obviously the
amount field that the methods change; the owner remains the same all the
time (given the methods of this example).
Your second task is to design the methods with the design recipe. For
applicative methods, follow the recipe that you know. For the design of
imperative methods, we need to modify the recipe a bit:
signature, purpose, & effect For an imperative method, your first task is
to clarify what the method consumes because you already know what
Section 27
380
it produces: void.54 Recall from the design recipe for applicative methods that a method always consumes at least one argument: this.
Once you have a signature, you can formulate a purpose and effect
statement. It concisely states what the method computes and how it
uses the result of the computation to change the object. This latter
part, dubbed effect statement, can take several forms; for now you
may just want to say which fields are changed.
Lets illustrate this step with the familiar example:
inside of Account :
// to deposit some amount of money into this Account
// effect: to add d to this.amount
void deposit(int d)
The signature says that the method consumes an int, which the purpose statement describes as an amount. From the effect statement, we
find out that the method changes what the amount field represents.
functional examples The purpose of the preceding step statement is to figure out what a method is to compute. When you have that, you formulate examples. Examples of the kind we have used in the past
wouldnt work, because they would all look like this:55
check anObject.imperativeMethod(arg1,. . . ) expect void
After all, void is the standard result of an imperative method.
Therefore instead of functional examples, you formulate behavioral examples. In contrast to a functional example, which specifies the expected result for given arguments, a behavioral example states what
kinds of effects you wish to observe after the method invocation returns void.
The simplest form of a behavioral example has this shape:
SomeClass anObject = new SomeClass(. . . )
54 Some people prefer to return the object itself from an imperative method, which makes
it is easy to chain method invocations. At the same time, it obscures the purpose statement because the method might just be an applicative method simulating changes with the
creation of new objects.
55 Warning: this syntax is illegal.
381
anObject.imperativeMethod(arg1,. . . );
check anObject.field1 expect newValue1
check anObject.field3 expect newValue2
That is, given an object whose properties you know because you just
constructed it, the invocation of an imperative method returns void.
Afterwards, an inspection of the properties ensures that the method
assigned changed the desired fields and left others alone.
Sometimes it is too time consuming to create a new object. In those
cases, you can observe the properties of an object and use them:
Type1 value1 = anObject.field1;
Type2 value2 = anObject.field2;
anObject.imperativeMethod(arg1,. . . );
check anObject.field1 expect newValue1
check anObject.field3 expect newValue2
In these check expressions, newValue1 and newValue2 are expressions
that may involve value1 and value2. Indeed, the two variables may
also be useful as arguments to the method call.
Last but not least, you are generally better off using applicative methods to inspect the imperative effects of a method because fields are
often private to a class:
Type1 value1 = anObject.method1();
Type2 value2 = anObject.method2();
anObject.imperativeMethod(arg1,. . . );
check anObject.method1() expect newValue1
anObject.method3() expect newValue2
Here method1, method2, and method3 are methods that do not change
the object. As before, they extract values that you may use in the
method arguments or in the tests.
Section 27
382
Generally speaking, the first half of your behavioral example determines the current state of the object, i.e., the values of its fields; the
second half specifies the expected changes.56
Lets see how this works for our running example:
Account a = new Account("Sally",10);
a.deposit(20);
check a.amount expect 30
If amount were private, we would have to use the balance method to
determine whether desposit works:
Account a = new Account("Sally",10);
a.deposit(20);
check a.balance() expect "Sally: 30"
This example suggests that you might want to add a method that
produces the balance as an int rather than as a part of a String.
Terminology: because of the role that applicative methods play in
this contex, they are also known as OBSERVER methods, or just observers. Likewise, imperative methods are known as COMMANDS.
template As always the objective of the template step is take stock of the
data to which the method has access. Remember that the template
enumerates the fields, the method parameters (standing in for the arguments), and their pieces if needed. For an imperative method, you
also enumerate the fields that the method should change.
Here is our running example enhanced with a method template:
56 If you also ensure that value1 and value2 satisfy certain conditions, people refer to the
first half as preconditions and the second half of the example as postcondition, though
these words are more frequently used for more general claims about methods than specific
examples.
383
inside of Account :
// to deposit some amount of money into this Account
// effect: to add d to this.amount
void deposit(int d) {
. . . this.amount = . . . this.amount . . . this.owner . . .
}
The amount field shows up twice: on the left of the assignment statement it is a reminder that we consider it a changeable field; on the
right it is a reminder that the data is available to compute the desired
result. As discussed, the owner field should never change.
method definition Now use the purpose and effect statement, the examples, and the template to define the method. Keep in mind that you
should work out additional examples if the ones you have prove insufficient. Also, if the task is too complex, dont hesitate to put additional observer and/or command methods on your wish list.
And indeed, the definition of deposit is straightforward given our
preparation:
inside of Account :
// to deposit some amount of money into this Account
// effect: to add d to this.amount
void deposit(int d) {
this.amount = this.amount + d;
return ;
}
tests Last but not least, you must turn the examples into automatic tests:
Testing
class Examples {
Account a = new Account("Sally",10);
boolean testDeposit() {
a.deposit(20);
Run the tests when the method and all of its helper methods on the
wish lists have been designed and tested.
Section 27
384
Basic Classes
A basic class comes with a name and fields of primitive type. In this case,
an applicative template enumerates the fields (and the arguments):
+-------------+
| Basic
|
+-------------+
| String s
|
| int i
|
| double d
|
+-------------+
| ??? mmm() { |
|
this.s
|
|
this.i
|
|
this.d } |
+-------------+
+-------------+
| Basic
|
+-------------+
| String s
|
| int i
|
| double d
|
+-------------+
| void mmm(){ |
|
this.s
|
|
this.i
|
|
this.d
|
|
|
| this.s =
|
| this.d = } |
+-------------+
A template for an imperative method differs from this in just one aspect:
for each changeable field, introduce an assignment statement with the field
on the left and nothing (or all of the fields) on the right. When you finally define the method, the template reminds you that you can use the
method arguments and the fields to compute the new value for the changing fields. Also keep in mind that an imperative method may change one
field or many fields.
Containment
A field b in class Containment may stand for objects of some other class, say
Basic. In this case, it is quite common that a method in Containment may
have to compute results using the information inside of the value of b:
+---------------+
| Containment
|
+---------------+
+-------------+
| Basic b
|------>| Basic
|
| double d
|
+-------------+
+---------------+
| String s
|
| ??? mmm() {
|
| int i
|
+---------------+
| Containment
|
+---------------+
+-------------+
| Basic b
|------>| Basic
|
| double d
|
+-------------+
+---------------+
| String s
|
| ??? mmm() {
|
| int i
|
| this.d
|
|
|
| this.b.s
|
| this.b.i
|
| this.b.d }
|
+---------------+
| double d
|
+-------------+
385
| this.b.nnn() |
| this.d }
|
+---------------+
| double d
|
+-------------+
| ??? nnn() { |
|
this.s
|
|
this.i
|
|
this.d } |
+-------------+
The template on the left says that your method could reach into the contained object via this.b.s etc. While this is indeed a possibility that we initially exploited, we have also learned that it is better to anticipate the need
for an auxiliary method nnn in Basic that helps mmm compute its result.
In principle, we have the same choices for imperative methods. An imperative mmm method in Containment can change what s, i, or d in Basic represent; we used this power to create circular object references in section 23.
For the same reasons as in the applicative case, however, it is preferable to
design auxiliary methods in Basic that perform these changes:
+---------------+
| Containment
|
+---------------+
+-------------+
| Basic b
|------>| Basic
|
| double d
|
+-------------+
+---------------+
| String s
|
| void mmm() { |
| int i
|
| this.b.nnn() |
| double d
|
| this.d =
|
+-------------+
| this.d }
|
| void nnn() {|
+---------------+
| this.s
|
| this.i
|
| this.d
|
| this.s =
|
| this.d = } |
+-------------+
Section 27
386
Unions
The third situation concerns unions of classes:
+-------------+
| IUnion
|
+-------------+
| void mmm() |
+-------------+
|
/ \
--|
+------------------+------------------+
|
|
|
+-------------+
+-------------+
+-------------+
| Basic1
|
| Basic2
|
| Basic3
|
+-------------+
+-------------+
+-------------+
| boolean b
|
| int i
|
| int i
|
| double d
|
| double d
|
| String s
|
+-------------+
+-------------+
+-------------+
| void mmm(){ |
| void mmm(){ |
| void mmm(){ |
|
this.b
|
|
this.i
|
|
this.i
|
|
this.d
|
|
this.d
|
|
this.s
|
|
this.b = |
|
this.i = |
|
this.i = |
|
this.d = }|
|
this.d = }|
|
this.s = }|
+-------------+
+-------------+
+-------------+
387
If you decide that a union must come with an imperative method, you add
the method signature and purpose & effect statement to the interface and
the method templates with bodies to the variants. The bodies of the templates contain the fields and assignments to the fields.
If you follow the advice for designing templates from the first three
cases and strictly apply it to Contain3, you get three pieces in the template:
1. this. u.mmm() is the recursive method invocation that is due to the
self-reference in the class diagram. Its purpose is to ask u to change
its fields as needed.
2. this.u is a reminder that this value is available.
3. this.u = . . . finally suggests that the method may change u itself.
Points 1 and 3 conflict to some extent. The method call changes fields in
u and possibly fields that are contained in objects to which u refers. When
this.u = . . . changes what the field stands for, it may throw away all the
computations and all the changes that the method invocation performed.
Even though the use of both forms of assignment is feasible, it is rare and
we wont pay much attention to the possibility in this book. The template
contains both and for the method definition, you choose one or the other.
Section 27
388
389
two similar methods. Thus, when you choose to work with stateful classes
and imperative methods, you may open additional opportunities for abstraction and you should use them.
27.5 Danger!
At first glance, stateful classes and imperative methods have three advantages over an applicative solution. First, they add expressive power; you
can now directly represent any form of relationship among a collection
of real world objects, even if they contain cycles. Second, they facilitate the representation of information that changes over time. Third, they
enable additional opportunities for abstraction, at least in the context of
Java and other popular object-oriented programming languages. Nothing
comes for free, however, and that includes stateful classes and imperative
methods. In this case, the very advantages can also turn into disadvantages
and stumbling blocks.
// creating directly circular objects
class Node {
private Node nxt = null;
public Node() {
this.nxt = this;
}
+-------------+
| Node
|<--+
+-------------+
|
| Node nxt
|---+
+-------------+
| int visit() |
+-------------+
Section 27
390
This expression has no result. If you solved exercises 23.8, you experienced
this problem in a slightly different, almost realistic setting.
The true problem is that this visit method is well-designed according to
the original design recipe. In particular, here is the template:
int visit() {
. . . this.nxt.visit() . . .
Because the class contains a single field, the template body naturally consists of a single expression. Furthermore, because the field has the same
type as the class, the design recipe requests an invocation of visit. The rest
appears to be a straightforward step. If you designed your length method
for exercise 23.8 in the same manner, you obtained a well-designed method
for a union of classes that you used quite often.
We overcome this problem through the use of a modified design recipe.
The major modification is to look out for those links in the class diagram
that suggest auxiliary links for cyclic references. Cutting themas suggested in figure 129, for examplerestores order and guarantees that the
original design recipe works. The step of cutting ties, however, isnt easy. It
is most difficult when your task is to revise someone elses program; your
predecessor may not have followed the design recipe. The class diagram
for the classes might be extremely complex; then it is easy to overlook one
of these links and to design a program that diverges. In short, with the
introduction of assignments you are facing new problems when you test
programs and when you are trying to eliminate errors.
The second disadvantage is due to the introduction of time and timing relationships into programming. While assignment statements make it
easy to represent changes over time, they also force programmers to think
about the timing of assignments all the time. More forcefully,
assignment statements are universally destructive.
Once an assignment has changed the field of an object that change is visible everywhere and, in particular, the old value of the field is no longer
accessible (through this field).
A traffic light simulation illustrates this point particularly well. Figure 140 (left, top) displays the three normal states of a traffic light in the
normal order. Initially, the red light is on, the others are off, with traffic
stopped.57 Then the green light comes on, with red and yellow off, en57 Turning
on the red light in the initial state (or in a failure state) is the safest possible
choice. Most physical systems are engineered in this manner.
391
abling the flow of traffic. Finally, the yellow light comes on to warn drivers
of the upcoming switch to red. From here, the cycle repeats.58
The right side of the figure displays the Bulb class. Its three properties
are: the size, the color, and the placement (on a canvas). The on and off
methods consume a Canvas and draw a disk or a circle of the given size
and at the specified position.
Now imagine that your manager asks you to design the TrafficLight
class, which consists of three Bulbs and which switches colors every so often. Clearly, this class represents a little world of its own, with changes
taking place every so often. Hence, you naturally design TrafficLight as an
extension of World from idraw. This decision requires the design of three
methods: onTick, draw, and onKeyEvent. Finally, the TrafficLight class has at
58 For our purposes, we ignore the failure state in which the yellow or red light is blinking.
Section 27
392
least three properties, namely the three bulbs: red, yellow, and green.
The class diagram on the bottom left of figure 140 summarize this data
analysis. The three Bulb-typed fields are called on, next, and third are named
to indicate which light is on, which comes next, and what the third one
is. Roughly speaking, the three fields represent a three-way light switch.
Obviously, the values of these fields must change over time to simulate the
transitions in the state of a traffic light. To complete the data design part of
the design recipe, determine appropriate initial values for these fields.
Let us now look at the design of onTick. Its purpose is to simulate a
transition in the traffic light, that is, to rotate the roles of the three lights. To
accomplish this rotation, the method affects the three Bulb fields:
if on is a red bulb, next is green, and third is yellow, the method
must change the fields so that on stands for green, next for yellow, and third for red.
When the draw method is invoked after onTick has finished its computation,
it will turn on the green bulb and turn off the other two.
We can translate what we have into this method header and template:
inside of TrafficLight :
// a transition from one state of the light to another
// effect: change the values of on, next, and third
void onTick() {
. . . this.on . . . this.next . . . this.third . . .
this.on = . . .
this.next = . . .
this.third = . . .
}
The first line reminds us that the method can use the values of the three
fields for its computation and the last three lines suggest that it can assign
new values to these three fields.
One apparently easy way to complete this template into a full definition
is to just place the three fields in a different order on the right side of the
assignment statements:
void onTick() {
this.on = this.next;
this.next = this.third;
this.third = this.on;
return ;
393
method entry
third:
on:
next:
this.on = this.next
third:
on:
this.next = this.third
next:
third:
on:
next:
this.third = this.on
third:
Section 27
394
Exercises
395
Exercise 27.1 Finish the design of the TrafficLight class. Run the traffic light
simulation. Would it make sense for Bulb objects to change color?
Exercise 27.2 Design an applicative version using draw. Compare and
contrast this solution with the one from exercise 27.1.
The third disadvantage concerns the information content of method signatures. The preceding section sketched how the use of void return types
increases the potential for abstraction. The reason is that methods that returned an instance of the class to which they belong may return void now
and may therefore look like another method in the same union.
Of course, this very fact is also a disadvantage. As this book has explained at many places, types in signatures tell the readers of the code what
and how the method communicates with its calling context. In the case of
void, we know that it does not communicate any values; it just changes
some fields. Worse, the language implementation (ProfessorJ or whatever
you use) checks types before it runs your program and points out (type)
errors. When the return type is void and there is nothing to check. In short,
the use of void disables even the minimal checks that type checkers provide.
You will notice this lack of basic checking when your programs become really large or when you collaborate on large projects with other people.
The overall lesson is one of compromise. Assignments are good for the
purpose of representing things that change over time. So use them, but use
them with care and keep their disadvantages and dangers in mind.
Section 27
396
397
Before we move on, recall that a method definition may always use
methods from the class itself; here, this means deposit and withdraw.
4. Picking up this last idea suggests an intuitive definition for transfer:
inside of Account :
// move the amount a from this account into the other account
// effect: change the amount field in this and other
void transfer(int a,Account other) {
this.withdraw(a);
other.deposit(a);
return ;
}
First the amount a is withdrawn from this account. Second, a is deposited in the other account. After that, the method returns void.
Exercises
Exercise 27.3 The design of transfer for the stateful Account class omits the
last step. Turn the example into a test and run the tests.
Exercise 27.4 According to the template, the transfer method could also directly assign new values to this.amount and other.amount. Define the transfer method in this way and compare it to the above definition. Which version do you think expresses the purpose statement better?
Section 27
398
// a pair of accounts
class PairAccounts {
Account first;
Account second;
+----------------+
| PairAccounts
|
+----------------+
| Account first |
| Account second |
+----------------+
this and other. Hence, the applicative version of transfer must produce two
instance of Account.
Unfortunately, methods in Java can only produce a single value,59 we
must somehow combine the two new objects into one. In other words, the
systematic design of the method forces us to introduce a new form of data:
pairs of Accounts. Figure 142 shows the rather standard design.
From here, the rest of the design follows from the design recipe:
1. Here is the complete method header:
inside of Account (applicative version) :
// move the amount a from this account into the other account
PairAccounts transfer(int a,Account other)
2. The adaptation of the above example requires very little work:
Account a = new Account("Matthew 1",100);
Account b = new Account("Matthew 2",200);
check b.transfer(100,a) expect
new PairAccounts(new Account("Matthew 1",200),
new Account("Matthew 2",100))
The advantage of the applicative form of the example is that everything is obvious: the names of the owners, the associated amounts,
and how they changed. Of course, the example is also overly complex compared with the imperative one.
59 In
Scheme and Common Lisp, functions may return many values simultaneously.
399
3. For the template we can almost reuse the one from the imperative
design, though we need to drop the assignment statements:
inside of Account (applicative version) :
// move the amount a from this account into the other account
PairAccounts transfer(int a,Account other) {
. . . this.amount . . . this.owner . . .
. . . other. lll() . . . other.amount . . .
}
4. The actual definition combines the example and the template:
inside of Account (applicative version) :
// move the amount a from this account into the other account
PairAccounts transfer(int a, Account other) {
return new PairAccounts(this.withdraw(a),other.deposit(a));
}
5. You should turn the example into a test and confirm that the method
works properly.
As you can see from this simple example, an applicative design can
easily produce a rather complex method. Here the complexity has two aspects. First, the method design requires the design of (yet) another class of
data. Second, the method must construct a complex piece of data to return
a combination of several objects. Here we could avoid this complexity by
designing the method from the perspective of the bank as a whole, changing several (presumably existing) classes at once. In general though, these
other classes are not under your control, and thus, applicative approaches
can lead to rather contorted designs.
Section 27
400
+------------------------+
# +------------------>| IShots
|<--------------------+
# |
+------------------------+
|
# |
| IShots move()
|
|
# |
| boolean draw(Canvas c) |
|
# |
| boolean hit(UFO u)
|
|
# |
+------------------------+
|
# |
|
|
# |
/ \
|
# |
--|
# |
|
|
# |
------------------------------|
|
# |
|
|
|
/ \
# |
+------------------------+
+------------------------+
|
--# |
| MtShots
|
| ConsShots
|
|
|
# |
+------------------------+
+------------------------+
|
========================= |
| IShots rest
|----+
|
|
| Shot first
|----+
|
|
+------------------------+
|
|
|
|
+------------------+
|
v
| UFOWorld
|
|
+------------------------+
+------------------------+
+------------------+
|
+--------->| UFO
|
| Shot
|
| int WIDTH
|
|
|
+------------------------+
+------------------------+
| int HEIGHT
|
|
|
| IColor colorUFO
|
| IColor colorShot
|
| IColor BACKG
|
|
|
| Posn location
|
| Posn location
|
| UFO ufo
|----|---+
+------------------------+
+------------------------+
| AUP aup
|----|---+
| UFO move()
|
| Shot move()
|
| IShots shots
|----+
|
| boolean draw(Canvas c) |
| boolean draw(Canvas c) |
+------------------+
|
| boolean landed()
|
| boolean hit(UFO u)
|
| World onTick()
|
|
| boolean isHit(Posn s) |
+------------------------+
| World onKeyEvent(|
|
+------------------------+
| String ke)
|
|
| boolean draw()
|
|
+------------------------+
| Shot shoot()
|
+--------->| AUP
|
| World move()
|
+------------------------+
+------------------+
| IColor aupColor
|
| int location
|
+------------------------+
| AUP move()
|
| boolean draw(Canvas c) |
| Shot fireShot()
|
+------------------------+
+------------------+
| World
|
+------------------+
| Canvas theCanvas |
+------------------+
| World onTick()
|
| World onKeyEvent(|
| String ke)
|
| World draw()
|
+------------------+
If we import World from the idraw package instead of the draw package, we are immediately confronted with three type problems: the overriding definitions of onTick, onKeyEvent, and draw in UFOWorld must now
produce void results, not Worlds. In particular, this suggests that onTick and
onKeyEvent can no longer produce new instances of the world, but must
modify this instance of UFOWorld to keep track of changes over time.
Lets deal with each method in turn, starting with draw:
401
Instead of combining the drawing methods with &&, the imperative version uses ; (semicolon), which implies that draw in UFO, AUP, and IShots
now produce void, too. Finally, draw returns void.
Exercise
Exercise 27.5 Modify the methods definitions of draw in UFO, AUP, and
IShots so that they use the imperative methods from Canavs in idraw.
The modifications concerning onTick are more interesting than that:
inside of UFOWorld (applicative) :
inside of UFOWorld (imperative) :
// move the ufo and the shots of
// move the ufo and the shots of
// this world on every clock tick
// this world on every clock tick
World onTick() {
void onTick() {
if (this.ufo.landed(this)) {
if (this.ufo.landedP(this)) {
return this.endOfWorld("You lost."); }
this.endOfWorld("You lost."); }
else { if (this.shots.hit(this.ufo)) {
else { if (this.shots.hit(this.ufo)) {
return this.endOfWorld("You won."); } this.endOfWorld("You won."); }
else {
else {
return this.move(); }
this.move(); }
}
return ;
}
}
}
The method first checks whether the UFO has landed, then whether the
UFO has been hit by any of the shots. In either case, the game is over. If
neither is the case, however, the method moves every object in the world,
using a local move method. Clearly, this part must change so that move no
longer produces a new UFOWorld but changes this one instead. Hence,
the imperative version of onTick on the right side doesnt look too different
from the applicative one. The true code modifications are those in move.
Section 27
402
inside of UFOWorld :
// move the UFO and shots
void move() {
this.ufo = this.ufo.move(this);
...
return ;
}
The one on the left assumes that UFO is stateful and its move method is
imperative. The definition on the right assumes that UFO is applicative
and that therefore its move method produces a new instance of UFO, which
UFOWorld keeps around until the next tick of the clock.
Now it is up to you to make a decision of how to design UFO. You know
from the applicative design (or from reading the problem statement) that
you need at least four methods for UFO:
1. move, which moves this UFO for every clock tick;
2. draw, which draws this UFO into a given Canvas;
3. landed, which determines whether the UFO is close to the ground; and
4. isHit, which determines whether the UFO has been hit by a shot.
According to our design principle, you should consider whether calling
some method influences the computations (results) of some other method
in the class. After a moment of reflection, it is clear that every invocation
of move changes what draw, landed, and isHit compute. First, where draw
places the UFO on the canvas depends on how often move was called starting with bigBang. Second, whether or not the UFO has landed depends on
403
404
Section 27
how far the UFO has descended, i.e., how often move has been called. Finally, the case for isHit is ambiguous. Since a shot moves on its own, the
UFO doesnt have to move to get hit; then again, as the UFO moves, its
chances for being hit by a shot change.
Our analysis suggests that designing UFO as a stateful class with move
as an imperative method is a reasonable choice. It is also clear that move
changes the location field of the class. Every time it is called it recomputes
where the UFO ought to be and assigns this new Posn to location.
Figure 144 displays the stateful version of UFO. The key method is move.
Its effect statement informs us that it may change the value of the location
field. The method body consists of an if-statement that tests whether the
UFO has landed. If so, it does nothing; otherwise it computes the next
Posn and assigns it to location. Both landed and isHit use the current value
of location to compute their answers. Because locations value continuously
changes with every clock tick, so do their answers. Finally, draw is imperative because we are using the idraw library now.
Exercises
Exercise 27.6 The definition of landed contains an invocation of distanceTo.
Design the method distanceTo and thus complete the definition of UFO. Is
distanceTo truly a method of UFO? Where would you place the definition in
figure 98 (see page 288)?
Exercise 27.7 Develop examples for the move method in UFO. Reformulate
them as tests.
Exercise 27.8 Develop examples and tests for landed and isHit in UFO.
Exercise 27.9 Design two imperative versions of shoot in UFOWorld: one
should work with a stateful version of AUP and another one that works
with the existing functional version of AUP. Also design the stateful version of the AUP class.
With the movement of UFOs under control, lets turn to the collection
of shots and study whether we should make it stateful or not. The IShots
interface specifies three method signatures: move, draw, and hit. By analogy, you may want to make the first imperative. The second is imperative
because the idraw library provides imperative draw methods. The question
405
is whether repeated calls to move affect the hit method, and the answer is
definitely yes. As the shots move, they distance to the UFO (usually)
changes and thus hit may produce a different answer.
Still, you should ask what exactly is stateful about moving all the shots.
After all, it is obvious that moving the shots doesnt change how many
shots there are. It also shouldnt change in what order the shots appear
on the list. With this in mind, lets follow the design recipe and see what
happens. Step 1 calls for a full signature, purpose and possibly effect statement:
inside of IShots :
// move the shots on this list; effect: fill in later
void move()
Since an interface doesnt have fields, it is impossible to specify potential
changes to fields. For step 2, we consider a two-item list of shots:
Shot s1 = new Shot(new Posn(10,20));
Shot s2 = new Shot(new Posn(15,30));
IShots los = new ConsShots(s1,new ConsShots(s2,new MtShots()));
los.move();
check s1.location expect new Posn(10,17)
check s2.location expect new Posn(15,27)
So far the example says that each of the shots on the list must change locations after the move method for IShots is invoked. It doesnt reflect our
thinking above that los per se doesnt change. To express this, you could
add the following line:
check los expect
new ConsShots(new Posn(10,17),
new ConsShots(new Posn(15,27),
new MtShots()));
Of course, this example subsumes the ones for s1 and s2.
The IShots interface is the union type for two variants: MtShots and
ConsShots, which means the template looks as follows:
Section 27
406
inside of MtShots :
// move these shots
void move() {
...
}
inside of ConsShots :
// move these shots
void move() {
. . . this.first.move() . . . this.rest.move() . . .
. . . this.first = . . .
. . . this.rest = . . .
}
The template for ConsShots looks complex. It reminds us that the method
can use this.first, can invoke a method on this.first, and that the natural
recursion completes the process. The two skeleton assignments suggest
that the method could change the value of the first and rest fields.
Put differently, the method template empowers us again to express a
design decision:
Should we use a stateful representation of lists of shots?
If the representation is stateful, the assignment for this.first changes which
Shot the first field represents. If the representation remains applicative,
however, the method must demand that the invocation this. first.move()
changes Shotwhich you have to add to your wish list or communicate
to the person who designs the new Shot class for you.
As discussed above, we dont think of the list per se as something that
changes. For this reason, we stick to an applicative class with an imperative
version of move, knowing that the imperativeness is due to the imperative
nature of move in UFOWorld and requires an imperative version of move in
Shot. Based on the examples and the template, we arrive at the following
two definitions:
inside of ConsShots :
inside of MtShots :
void move() {
void move() {
return ;
this.first.move();
}
this.rest.move();
return ;
}
That is, the move method in MtShots does nothing and the one in ConsShot
invokes an imperative move method in Shot. The design of move thus adds
an imperative move method for Shot on the wish list, re-confirming that Shot
is stateful. At the same time, we can now remove the effect statement for
move in IShots.
The first step in the design of this final move method is to write down
its signature and purpose and effect statement:
407
inside of Shot :
// move this Shot
// effect: change location
void move()
The effect statement states the obvious: when an object move, its location changes. Since the example for moving a list of shots covers the move
method for Shot, too, we go right on to the template:
void move() {
. . . this.location . . . this.colorShot . . .
this.location = . . .
The rest is easy and produces a method definition quite similar to the one
in UFO (see figure 144).
Exercises
Exercise 27.10 Complete the definition of move in Shot and in UFOWorld.
Create examples for the latter, and turn all the examples into tests.
Exercise 27.11 Complete the definitions of draw and hit in the MtShots, ConsShots, and Shot classes. If you have done all the preceding exercises in this
section, you now have a complete War of the Worlds game and youre
ready to play.
Exercise 27.12 Revise figure 143 so that it is an accurate documentation of
the imperative version of the program.
Equip all pieces of all classes with privacy specifications.
Exercise 27.13 Design a solution to the War of the Worlds game that relies on a stateful version of the list of shots and keeps the Shot class applicative. Compare and contrast your solution with the one above.
Exercise 27.14 Modify the design of the AUP class so that the AUP moves
continuously, just like the UFO and the shots, i.e., it acts like a realistic
vehicle. Let the design recipe guide you. Compare with exercise 19.19.
Exercise 27.15 Design UFOs that can defend themselves. The UFO should
drop charges on a random basis and, if one of these charges hits the AUP,
the player should lose the game. A charge should descend at twice the
speed of the UFO, straight down from where it has been released. Compare
with exercise 19.20.
Section 27
408
Exercise 27.16 Re-design the Worm game from section 19.9 in an imperative manner, using the guidelines from this chapter.
game, published by US Game Systems, resembles Rummy but uses cards labeled
with Axis and Allies World War II war planes.
409
You are told to produce an implementation of decks of cards while following the guidelines of this book. Someone else has already been asked to
produce an implementation of playing cards, but since the actual nature of
cards dont really matter to decks, you are to use the ICard interface and
stub class from figure 145.61
The first part of the design recipe is about the identification of the different kinds of information that play a role in your problem statement. For
your problemthe simulation of a deck of cardsthere appears to be just
one class of information: the deck of cards. Without any further information, we can think of the deck of cards as a list of cards, especially since the
sequence matters according to the problem statement. Given how routine
the design of a list is, we just assume the existence of ICards, MtCards and
ConsCards for now, without looking at the details at all.
As you now consider whether the deck of cards should be applicative or
a stateful, you must take a look at the methods that you might need. From
the problem statement, you know that the game program creates the deck
and that (the program piece representing) the players can do three things
to the deck afterwards:
1. put a card back on top of the deck, and
2. remove a number of cards from the top of the deck,
3. look at the first card of the deck.
Of course, the second item implies that a player can (and must be able to)
count how many cards there are on the deck, so we add this method as a
fourth one:
4. count the number of cards in a deck.
The following names for these four methods offer themselves naturally:
put, take, look, and count.
The question is whether calling one of these methods influences the
computation or the result of any of the other. Consider put, which consumes an ICard and adds it to the top of a deck. Of course this should
change the result of look, which shows the top-most ICard of a deck. And
it should also change the result of count, which computes how many cards
are in a deck. The case of take is similar. Its task is to remove cards from the
deck, and naturally, this kind of action changes what the top-most card is
and how many cards there are in the deck of cards.
61 That
Section 27
410
Problem is that there are no obvious fields in a list that can represent the
changes that put and take are to compute. First, neither ICards nor MtCards
have fields. Second, the fields in ConsCards are to represent the first ICard
and the ones below the first. When a player puts a card on top of a deck, this
list doesnt change, only the deck that represents the cards. Put differently a
card is added to the front of the list with new ConsCards(. . . ) and somehow
the deck changes. Similarly, when a player takes some cards, it is not that
first or rest should change; only the deck changes.
Our analysis thus produced two insights. On one hand, a deck of cards
needs a list of cards. On the other hand, a deck isnt just a list of cards.
Instead, a deck should consist of a list of cards and the above methods. In
short, Deck is a class with one assignable fieldcontentand four methods,
two of whichput and takemay assign to this field.
class Deck {
ICards content . . . ;
Deck(. . . ) { . . . }
// place one card on top of this deck
// effect: change content to include the
// given card
void put(ICard t) { . . . }
// take the first i cards from this deck
// effect: change content to indicate the
// removal of the cards
ICards take(int i) { . . . }
+-------------------+
| Deck
|
+-------------------+
+--------+
| ICards content
|------->| ICards |
+-------------------+
+--------+
| void put(Card c) |
|
| ICards take(int n)|
/ \
| int count()
|
--| ICard look()
|
|
+-------------------+
/ \
/
\
/
\
.........
411
Therefore the next step is to inspect the problem statement for an example of a real world deck of cards. As it turns out, the very first sentence
explains how the deck comes about or, in programming terminology, how it
is created. The game administrator places a single card on the table, which
starts the deck. This suggests that Decks constructor consume a single card
and from this creates the deck:
inside of Deck :
Deck(ICard c) {
this.content = new ConsCards(c,new MtCards());
}
That is, the constructor creates a list from the given card and uses this list
to initialize the content field.
Discussion: Given this constructor for Deck, you may be wondering
whether we shouldnt design a data representation for non-empty lists.
Take a second look at the problem statement, however. When it is a players
turn, one action is to take all cards from the deck. This implies that during a
turn, the deck could be temporarily empty. It is only at the end of the turn
that the deck is guaranteed to be non-empty.
Using this constructor, you can now create sample objects for Deck:
new Deck(new Card("Boeing B-17 (Flying Fortress)",2))
In the actual game, this would be a card showing the second view of the
Flying Fortress airplane. Unfortunately, the natural constructor doesnt allow us to build a representation of a deck of many cards. To do so, we need
to design put, whose purpose it is to put several cards to atop the deck:
ICard c1 = new Card("Boeing B-17 (Flying Fortress)",2);
ICard c2 = new Card("Boeing B-29 (Superfortress)",1);
Deck d1 = new Deck(c1);
d1.put(c2)
check d1.content
expect new ConsCards(c2,new ConsCards(c1,new MtCards()))
This behavioral example constructs two cards, uses one to construct a Deck,
puts the other one on top of the first, and then ensures that this is true with
a look at the content field. If content were private, you could use look instead
to inspect the deck:
check d1.look() expect c2
Section 27
412
After all, it is its purpose to uncover the top-most card, and in d1, the topmost card is the last one added.
The reformulation of the example appears to suggest a switch to the
design of look so that we can run tests as soon as possible. While this is an
acceptable strategy in some cases, this particular case doesnt require a consideration of the auxiliary method. The template of put is straightforward.
It consists of this.content.mmm() and a skeleton assignment to content:
inside of Deck :
void put(ICard c) {
. . . this.content.mmm() . . .
this.content = . . .
}
The former reminds us that the value of content is available for the computation and that we may wish to define an auxiliary method in ICards; the
latter reminds us that the method may assign a new value to the field.
Since the goal is to add c to the front of the list, the actual method just
creates a list from it and this.content and assigns the result to content:
inside of Deck :
void put(ICard c) {
}
No new wish for our wish list is required.
Equipped with put, you can now create a data representation of any actual deck of cards. In other words, the design recipe for a data representation and the design recipe for methods are intertwined here. Furthermore,
the design of put implicitly created a behavioral example for look.
The template for look is like the one for put, minus the assignment:
inside of Deck :
ICard look() {
. . . this.content.mmm() . . .
}
Based on the examples, look is to extract the first card from the list. Hence,
we add a fst method62 to the wish list for ICards and define look as follows:
62 In Java proper, we could name the method first. Because ConsCards implements ICards,
the class must then include both a field called first and a method called first. Java distinguishes methods and fields and can thus reason separately about those and can (usually)
disambiguate between the two kinds of references.
413
inside of Deck :
ICard look() {
return this.content.fst()
}
Judging from counts purpose statement in figure 146, the first data example is easily turned into a behavioral example:
ICard c1 = new Card("Boeing B-17 (Flying Fortress)",2);
Deck d1 = new Deck(c1)
check d1.count() expect 1
The other example from above is also easily modified for count:
ICard c1 = new Card("Boeing B-17 (Flying Fortress)",2);
ICard c2 = new Card("Boeing B-29 (Superfortress)",1);
Deck d1 = new Deck(c1);
d1.put(c2)
check d1.count() expect 2
Not surprisingly counts template is almost identical to looks:
inside of Deck :
int count() {
. . . this.content.mmm() . . .
}
After all, like look, count is an observer and has no effects. Of course, the
reference to content, a list of cards, implies that count should just delegate
the task of counting cards to the list-of-cards representation. Put differently,
we are adding a second wish to our wish list: length for ICards.
This is the proper time to stop and to write down the list representation
for the list of cards and the wish list of methods on lists: see figure 147. It
displays the class diagram for lists of cards and it uses the interface box for
writing down the wish list. Specifically, the interface for ICards lists the two
methods we have wished for so far: fst and length. The last two methods
are needed for the take method, which we are about to design.
Exercises
Exercise 27.17 Develop purpose statements for the fst and length methods
in the ICards interface.
Section 27
414
+------------------------------------------+
| ICards
|<-------------+
+------------------------------------------+
|
+------------------------------------------+
|
| // the first card of this list
|
|
| ICard first()
|
|
| // the length of this list
|
|
| int length()
|
|
| // pick the first i cards of this list
|
|
| ICards pick(int i)
|
|
| // drop the first i cards from this list |
|
| ICards drop(int i)
|
|
+------------------------------------------+
|
|
|
/ \
|
--|
|
|
------------------------|
|
|
|
+-------------+
+------------------+
+------------------+
|
+->| ICard
|
| MtCards
|
| ConsCards
|
|
| +-------------+
+------------------+
+------------------+
|
|
| ICard first
|--------------------------|---+
| ICards rest
|--------------------------+
+------------------+
Exercise 27.18 Design fst and length for list of cards representation. Then
convert the examples for put, look, and count from Deck into tests and run
the tests. Make sure to signal an error (with appropriate error message)
when fst is invoked on an instance of MtCards.
Our last design task is also the most complex one. The take method is
to remove a specified number of cards from the top of the deck.
In figure 146, this sentence has been translated into a purpose and effect
statement plus a method signature. Lets look at those closely:
inside of Deck :
// take the first i cards from this deck
// effect: change content to indicate the removal of the cards
ICards take(int i)
The English of the purpose statement is ambiguous. The take could mean
to remove the specified number of cards and to leave it at that, or it could
mean to remove them and to hand them to the (software that represents
the) player. The presence of the effect statement clarifies that the second
option is wanted. That is, the method is to remove the first i cards from
415
the deck and simultaneously return them in the form of a list. In short, the
method is both a command and an observer.
Now that we have a clear understanding of the role of take, making up
behavioral examples is feasible. Lets start with the simple data example:
ICard c1 = new Card("Boeing B-17 (Flying Fortress)",2);
Deck d1 = new Deck(c1)
check d1.take(1) expect new ConsCards(c1,new MtCards())
Since take is both an observer and a command, it is possible to specify its
desired result and to specify its applicative behavior. Still, ensuring that
the method invocation also affects the contents field requires additional observations (or post-conditions):
check d1.count() expect 0
This line demands that after taking one card from a singleton deck, there
shouldnt be any cards left, i.e., count should produce 0. Of course, this
immediately raises the question what look is supposed to return now:
d1.look()
The answer is that a player shouldnt look at the deck after requesting (all
the) cards and before putting the end-of-turn card down. The situation corresponds to a request for the first item of a list and, as we know, evaluation
the Scheme expression (first empty) raises an error. It is therefore appropriate for look to produce an error in this case.
For a second behavioral example, we use the deck of two cards:
ICard c1 = new Card("Boeing B-17 (Flying Fortress)",2);
ICard c2 = new Card("Boeing B-29 (Superfortress)",1);
Deck d1 = new Deck(c1);
d1.put(c2)
This deck has two cards, meaning take can remove one or two cards from
it. Lets see how you can deal with both cases. First, the case of taking one
card looks like this:
check d1.take(1) expect new ConsCards(c2,new MtCards())
check d1.count() expect 1
check d1.look() expect c1
Section 27
416
Second, the case of taking two cards empties the deck again:
d1.take(2) expect new ConsCards(c2,new ConsCards(c1,new MtCards()))
d1.count() expect 0
And as before, d1.look() raises an error.
The template for take is similar to the one for put:
inside of Deck :
ICards take(int n) {
this.content = . . .
. . . this.content.mmm(n) . . .
}
Together, the template, the examples, and the header suggest the following:
inside of Deck :
ICards take(int n) {
this.content = this.content.drop(n);
return this.content.pick(n);
}
Before you continue now you should turn the examples into tests, complete
the program, and run all the tests.
Exercises
Exercise 27.19 At this point the wish list contains two additional methods
for ICards (also see figure 147):
1. pick, which retrieves a given number of cards from the front of a list;
2. and drop, which drops a given number of cards from the front of a list
and returns the remainder.
Design these two methods. Then turn the examples for take into tests and
run the tests. Observe that tests fail. Explain why.
Exercise 27.20 Abstract over the two flavors of test cases with two items in
a deck. That is, create a method that produces a deck with two cards and
reuse it for the test cases.
417
When you run the tests for this first definition of take, the method invocation d1.take(2) signals an error even though d1 contains two cards. ProfessorJ also shows you that it is the second line in takes method body that
causes the error: pick is used on an empty list of cards. The problem is that
this.content = this.content.drop(n)
has already changed the value of the content field by the time, the pick
method is used to retrieve the front-end of the list. In short, we have run
into one of those timing errors discussed in section 27.5.
To overcome the timing error, it is necessary to compute the result of
take before changing the content field:
inside of Deck :
ICards take(int n) {
ICards result = this.content.pick(n);
this.content = this.content.drop(n);
return result;
}
Following section 27.5, the method uses a temporary variable, named result, to hang on to the demanded items from content. Afterwards it is free
to change content as needed. At the end, it returns the value of result.
Exercises
Exercise 27.21 Formulate the examples for take as test cases and complete
the definition of Deck.
Exercise 27.22 The expression
new Deck(new Card("B-52",4)).take(2)
signals an error concerning lists. This should puzzle your colleague who
uses your Deck class to implement an all encompassing GameWorld class.
Here, he has accidentally invoked take with a number that is too large for
the given Deck, but this still shouldnt result in an error message about lists.
Modify the definition of take so that it signals an error concerning Decks if
invoked with bad arguments.
Exercise 27.23 Find all places where our design of decks raise errors. Formulate a boolean expression, using observer methods from the same class,
Section 27
418
that describes when the method is guaranteed to work properly. Add those
expressions to the purpose statement prefixed with @pre.
Note: Such conditions are known CONTRACTS. Ideally, a language
should help programmers formulate contracts as part of the method signature so that other programmers know how to use the method properly
and, if they fail, are told what went wrong (where).
Exercise 27.24 Explain why the game administrator can always look at the
Deck between the turns of players without running the risk of triggering an
error.
27.8.1
// a deck of cards
class Deck {
private ICards content = new MtCards();
// a cache for tracking the number of cards
419
public Deck(ICard c) {
this.content = new ConsCards(c,new MtCards());
this.noCards = 1 3 ;
}
public void put(ICard t) {
this.content = new ConsCards(t,this. content);
this.noCards = this.noCards+1 4 ;
return ;
}
public ICards take(int i) {
ICards result = this.content.take(i);
this.content = this.content.drop(i);
this.noCards = this.noCards-i 5 ;
return result;
}
public int count() {
return this.noCards 6 ;
}
...
4. The put method adds a card to content and therefore increments noCards by 1.
5. The take method removes i cards from content and must decrement
noCards by i.
6. Finally, count simply returns the value of noCards because it stands for
the number of cards in the field.
As you can see, modifying the class is straightforward. To ensure that no-
Section 27
420
Cards always stands for the number of cards in content, we check all effect
statements of the methods; if it mentions content, we must edit the method.
Exercises
Exercise 27.25 Now that Deck caches its number of cards, it is possible to
simplify the list representation. Do so.
Exercise 27.26 A cache doesnt necessarily involve a mutable field. Add
hidden fields to MtCards and ConsCards that represent how many cards the
list contains.
27.8.2
Every invocation of take in Deck triggers two traversals of the list of cards
to the exact same point. First, it uses take on the list of cards to produce a
list of the first i cards. Second, drop traverses the first i ConsCards again and
produces the list in rest of the ith object.
While these traversals take hardly any time, the two results can be computed with a single method, and you know in principle how to do so
with an applicative method. This example, however, provides a chance
to demonstrate the usefulness of assignments to self-referential fields such
as rest in ConsCards. Lets state the goal explicitly:
. . . Design the method break for ICards. The method consumes
an int i. Its purpose is to break the list, after the ith object on the
list; it is to return the second half as its result. . . .
The problem statement includes a purpose statement and suggests an effect
statement, too. All thats left for you here is to add a method signature:
inside of ICards :
// deliver the tail after the first i cards from this list
// effect: change rest in the i ConsCards so that it is MtCards()
ICards break(int i);
Since splitting a list is only interesting when a list contains some cards,
we start with a list that has two and split it in half:
ICard c1 = new Card("black",1);
ICard c2 = new Card("black",2);
ICards cs = new ConsCards(c1,new ConsCards(c2,new MtCards()));
421
inside of ConsShots :
ICards split(int i) {
. . . this.first . . .
. . . this.rest.split(. . . i . . . ) . . .
this.rest = . . .
}
The templates for both variants of the union are unsurprising. Because
MtCards has no fields, its template doesnt contain any expressions. In ConsCards, we see three expressions: one that reminds us of the first field; one
for rest and the natural recursion; and a skeleton assignment to rest.
At this point, we are almost ready to define the method. What we are
missing is a thorough understanding of i, the second argument. Up to now,
we have dealt with i as if it were an atomic piece of data. The purpose
statement and examples, though, tell us that it is really a natural number
and split processes the list of cards and this number in parallel. From How to
Design Programss chapter III, we know that this requires a conditional that
checks how the list gets shorter and the number smaller. In Java, however,
there are no tests on the lists. Each list-implementing class contains its own
split method so that it knows whether the list is empty or not.
In other words, the methods must only test how small i is and must
decrease i when it recurs. When i becomes 1, the method has traversed
exactly i instances of ConsCards, and it must split the list there:
Section 27
422
inside of ConsShots :
ICards split(int i) {
if (i == 1) {
. . . this.first . . .
this.rest = . . .
} else {
return this.rest.split(i); }
}
Note how the then branch in this outline composes two sketches of statements. In a world of effects, this makes perfect sense.
The method outline translates the words into code. The if-statement
distinguishes two cases:
(i == 1) In this case, the method is to traverse one instance of ConsCards
and has just done so. There is no need to continue the traversal, which
is why the natural recursion has been removed from method template, which is otherwise repeated unchanged.
(i != 1) In this case, the method hasnt traversed enough nodes yet, but
it has encountered one more than before. Hence, it uses the natural
recursion from the template to continue.
Given this case analysis, we can focus on the first case because the second
one is done. A first guess is to use
if (i == 1) {
this.rest = new MtCards();
return this.rest; }
else . . .
But obviously, this is nonsense. Return this.rest right after setting it to new
MtCards() cant possibly work because it always return the empty list of
cards. We have encountered another case of bad timing. To overcome this
problem, we introduce a local variable and save the needed value:63
ICards result = this.rest;
if (i == 1) {
this.rest = new MtCards();
return result; }
else . . .
63 In
Java, the variable could be made local to the then branch of the if-statement.
423
In this version, the value of this.rest is saved in result, then changed to new
MtCards(), and the method returns the value of result.
For completeness, here are the definitions of the two split methods:
inside of MtCards :
ICards split(int i) {
return
Util.error("... an empty list");
}
inside of ConsShots :
ICards split(int i) {
if (i == 1) {
ICards result = this.rest;
this.rest = new MtCards();
return result;
} else {
return this.split(i-1); }
}
Exercises
Exercise 27.27 Turn the examples for split into tests and ensure that the
method definition works properly for the examples. Add tests that explore
what split computes when given 0 or a negative number.
Exercise 27.28 Use the split method to re-define take in Deck.
Exercise 27.29 Challenge problem: Design splitA, an applicative alternative to split. Like split, splitA consumes an int i. After traversing i instances
of ConsCards, it returns the two halves of the list.
Hint: See section 27.6 on how to return two results from one method.
Also re-read How to Design Programs (chapter VI) on accumulator style.
cannot really explain how to define either of the two libraries in complete detail.
For that, you will need to learn more about computers and operating systems.
Section 27
424
+------------+
| draw.World |
+---------------------------------------+
| Canvas theCanvas
|
+---------------------------------------+
| abstract World onTick()
|
| abstract World onKeyEvent(String ke) |
| abstract boolean draw()
|
| World endOfWorld(String s)
|
| boolean bigBang(int w,int h,double c) |
+---------------------------------------+
|
/ \
--|
=============================================================================================
|
+------------------------------------+
+-----------------------------------------+
| abstract SWorld
|<-1-+ +--1->| AuxWorld
|
+------------------------------------+
+-|-+
+-----------------------------------------+
| AuxWorld aw
|--1---+ +-1-| SWorld sw
|
| Canvas theCanvas
|
+-----------------------------------------+
+------------------------------------+
| World onTick()
|
| abstract void onTick()
|
| World onKeyEvent(String ke)
|
| abstract void onKeyEvent(String ke)|
| boolean draw()
|
| abstract void draw()
|
| Canvas bigBangC(int w, int h, double c) |
| void endOfWorld(String s)
|
+-----------------------------------------+
| void bigBang(int w,int h,double c) |
+------------------------------------+
|
/ \
--|
=============================================================================================
|
subclasses of SWorld (students programs)
425
Exercises
Exercise 27.30 Why cant SWorld extend World directly? After all, World
provides all the needed functionality, and programmers are supposed to
extend it to use it.
Exercise 27.31 Design the class Block1 as a subclass of SWorld. The purpose
of Block1 is to represent a single block dropping from the top of a 100 by 100
canvas and stopping when it reaches the bottom. Make the class as simple
as possible (but no simpler).
Note: This exercise plays the role of the example step in the design
of SWorld. To experiment with this class, switch it to be a subclass of World
from idraw.
The box on the right side of the library tier in figure 149 is needed according to the design guidelines for using existing abstract classes. Remember that according to chapter III, the use of an abstract class such as
World from idraw requires the construction of a subclass of World. The
subclass inherits bigBang, endOfWorld, and theCanvas from World; it must
provide concrete definitions for onTick, onKeyEvent, and draw. These last
three methods are to implement the behavior that creates the animation.
Of course, this behavior is really created by the users of the library, i.e., by
instances of subclasses of SWorld.
In turn, we know that the SWorld class is abstract; it introduces three
abstract methods: onTick, onKeyEvent, and draw; and it supplies bigBang,
endOfWorld, and theCanvas to its subclasses. These last three, it must somehow acquire from AuxWorld, the concrete subclass of World. These consid-
Section 27
426
erations imply that (some instances of) the two to-be-designed classes need
to know about each other.
Figure 149 shows this dependence with two fields and two containment
arrows. The AuxWorld class contains a field called sw with type SWorld, and
SWorld contains a field called aw with type AuxWorld. The methods in the
two classes use these two fields to refer to the other class and to invoke the
appropriate methods from there.
Now that we understand the relationship among the two classes, we
can turn to the next step in the design recipe, namely, figuring out how to
establish the relationship. To do so, lets imagine how we would create an
animation with this library. As agreed, an animation class (such as Block1
from exercise 27.31) is a subclass of SWorld. From the last two chapters,
you also know that the animation is started in two steps: first you create
the world with new and then you invoke bigBang on this object. Hence,
instances of (a subclass of) SWorld come into existence first. As they do,
they can can create the associated instance of AuxWorld and establish the
backwards connection.
Since all of this suggest that we need exactly one instance of AuxWorld
per instance of SWorld, we create the former and immediately associate it
with aw in the field declaration:
abstract class SWorld {
AuxWorld aw = new AuxWorld(this);
...
}
By handing over this to the constructor of AuxWorld, the latter can assign
its sw field the proper value:
class AuxWorld extends World {
SWorld sw;
AuxWorld(SWorld sw) {
this.sw = sw;
}
...
}
It is easy to see that the field declaration creates one instance of AuxWorld
per instantiation of SWorld and that this instance is associated with exactly
this instance of (a subtype of) SWorld.65
65 This
design demonstrates that even though the instances of SWorld and AuxWorld are
in a direct cyclic relationship, it isnt necessary to use the entire design process from the first
427
theCanvas = aw.bigBangC(w,h,c);
return ;
}
void endOfWorld(String s) {
aw.endOfWorld(s);
return ;
World onTick() {
ad.onTick();
return this;
}
}
abstract void onTick();
boolean draw() {
ad.draw();
return true;
With the fields and the class relationships mostly under control, we can
turn to the design of methods. Lets start with bigBang, which is the first
method that is invoked after SWorld is instantiated. Even though we cant
make concrete examples, we can lay out the template for the method:
section of this chapter. When there is a direct one-to-one, immutable relationship where one
object completely controls the creation of the other, it is possible to create the relationship
when the objects are created.
Section 27
428
inside of SWorld :
void bigBang(int w, int h, double c) {
. . . this.aw.mmm() . . . this.theCanvas.mmm() . . .
this.aw = . . .
this.theCanvas = . . .
}
Given that the class contains two fields of complex type, it is natural that
both show up as potential method calls. For completeness, we have also
added two assignment statements. Because we already know that aw provides the proper functionality with its bigBang method, it is tempting to just
call the method, wait for it to return true, and to return void in the end:
inside of SWorld :
void bigBang(int w, int h, double c) {
this.aw.bigBang(w,h,c); // throw away the value of this expression
return ;
}
Although this action starts the clock and creates (displays) the canvas, it
fails to make theCanvas available to this instance of SWorld. While the bigBang method in World assigns the newly created Canvas to theCanvas, the
theCanvas field in SWorld is still not initialized. Worse, theCanvas in World is
protected, meaning only the methods in World and in its subclasses can access its value. The SWorld class, however, cannot grab the value and assign
it to its theCanvas field.
One solution is to add a getCanvas method to AuxWorld whose purpose
is to hand out theCanvas when needed. It is important though that you
never call this method before bigBang has been called. An alternative solution is to define the method bigBangC, which invokes bigBang and then
returns theCanvas. By lumping together the two steps in one method body,
we ensure the proper sequencing of actions.
Figure 150 displays the complete definitions of SWorld and AuxWorld.
The code drops the this. prefix where possible to make the code fit into
two columns; it is legal and convenient to do so in the Advanced language
of ProfessorJ and Java. The figure also shows how AuxWorlds concrete
methods compute their results via method calls to the abstract methods
of SWorld. We have thus implemented a template-and-hook pattern (see
section 18.4, page 238) across two classes.
Exercises
429
Exercise 27.32 Equip all fields and methods in SWorld and AuxWorld with
privacy specifications and ensure that the classes work as advertised with
your Block1 animation (exercise 27.31).
Exercise 27.33 Replace the bigBangC method with a getCanvas method in
AuxWorld and adapt SWorld appropriately.
+-------------+
| idraw.World |
+-------------------------------------+
| Canvas theCanvas
|
+-------------------------------------+
| abstract void onTick()
|
| abstract void onKeyEvent(String ke) |
| abstract void draw()
|
| void endOfWorld(String s)
|
| void bigBang(int w,int h,double c) |
+-------------------------------------+
|
/ \
--|
===============================================================================================
|
+---------------------------------------+
+----------------------------------------+
| abstract AWorld
|<*-+ +-1-->| Keeper
|
+---------------------------------------+
+--|-+
+----------------------------------------+
| Keeper wk
|-*----+ +-1-| AWorld crnt
|
| Canvas theCanvas
|
+----------------------------------------+
+---------------------------------------+
| void onTick()
|
| abstract AWorld onTick()
|
| void onKeyEvent(String ke)
|
| abstract AWorld onKeyEvent(String ke) |
| void draw()
|
| abstract boolean draw()
|
| Canvas bigBangC(int w, int h, double c)|
| AWorld endOfWorld(String s)
|
+----------------------------------------+
| boolean bigBang(int w,int h,double c) |
+---------------------------------------+
|
/ \
--|
=============================================================================================
|
subclasses of AWorld (students programs)
Section 27
430
inside of Keeper :
void onTick() {
crnt = crnt.onTick();
...
crnt.draw();
}
431
inside of Keeper :
void onKeyEvent(String ke) {
crnt = crnt.onKeyEvent(ke);
...
crnt.draw();
}
Both methods compute their results in the same way. First, they invoke the
respective method on crnt and assign the resulting new world to crnt. Second, the methods invoke crnt.draw to draw the new world into the canvas.
Naturally, the onTick method in Keeper invokes crnt.onTick and the onKeyEvent method invokes crnt.onKeyEvent in the process. Remember that
crnt is an instance of (a subtype of) AWorld and that the onTick and onKeyEvent methods in AWorld are abstract. A subclass of AWorld overrides
these methods with concrete methods and those definitions are the ones
that the two methods above end up using.
While these first drafts implement the desired behavior from the external perspective of onTick, they fail to re-establish the connection between
this instance of Keeper and the worlds that are created. That is, when
crnt.onTick() creates new instance of (a subtype of) AWorld, it also automatically creates a new instance of Keeper, which is unrelated to this. Worse, the
new instance of AWorld doesnt know about theCanvas either because it is
this Keeper that holds on to the canvas with a picture of the current world.
Ideally, the evaluation of crnt.onTick() should not create a new instance
of Keeper. Of course, the constructor of an applicative class cannot distinguish the situation where a new world is created for which bigBang is about
to be called and the situation where the successor of a current world is created. Inside the body of this Keepers onTick method the situation is clear,
however. Hence, it is its duty to establish a connection between the newly
created world and itself (and its canvas):
inside of Keeper :
void onTick() {
crnt = crnt.onTick();
crnt.update(this,theCanvas);
crnt.draw();
}
inside of Keeper :
void onKeyEvent(String ke) {
crnt = crnt.onKeyEvent(ke);
crnt.update(this,theCanvas);
crnt.draw();
}
In other words, thinking through the problem has revealed the need for a
new method, an entry on our wish list:
Section 27
432
inside of AWorld :
// establish a connection between this (new) world and its context
// effect: change myKeeper and theCanvas to the given objects
void update(Keeper k, Canvas c)
That is, we need a method for establishing the circularity between AWorlds
and the keeper of all worlds after all.
Exercises
Exercise 27.34 Define update for AWorld. Collect all code fragments and
define AWorld and Keeper.
Now develop the subclass AppBlock1 of AWorld. The purpose of AppBlock1 is the same as the one of Block1 in exercise 27.31: to represent a single block that is dropping from the top of a canvas and stopping when it
reaches the bottom. Parameterize the public constructor over the size of the
canvas so that you can view differently sized canvases.
Use AppBlock1 to check the workings of AWorld and Keeper.
Exercise 27.35 The purpose of update in AWorld is to communicate with
Keeper. Use the method to replace bigBangC in Keeper with bigBang, i.e., a
method that overrides the method from World in idraw. Use Block1App
from exercise 27.34 to run your changes.
Equip all fields and methods in AWorld and Keeper with privacy specifications making everything as private as possible. Use AppBlock1 from
exercise 27.34 to ensure that the classes still work.
Argue that it is wrong to use public or protected for the privacy specification for update. Naturally it is also impossible to make it private. Note:
It is possible to say in Java that update is only available for the specified
library but not with your knowledge of privacy specifications. Study up
on packages and privacy specifications (and their omission) in the Java
report to clarify this point.
Exercise 27.36 Create an instance of Block1App, invoke bigBang on it, and
do so a second time while the simulation is running. Specify a canvas for
the second invocation that is larger than the first one. Explain why you can
observe only the second invocation before you read on.
Modify your definition of Block1App so that it works with the regular
draw library. Conduct the experiment again.
433
Exercise 27.36 exposes a logical error in our design. The very principle
of an applicative design is that you can invoke the same method twice or
many times on an object and you should always get the same result. In
particular, an applicative class and method should hide all internal effects
from an external observer. For the particular exercise, you ought to see two
canvases, each containing one block descending from the top to the bottom;
the re-implementation using World from draw confirms this.
The problem with our first draft of AWorld is that a second invocation
of bigBang uses the same instance of Keeper. If, say, the clock has ticked five
times since the first invocation of bigBang, the crnt field contains
b.onTick().onTick().onTick().onTick().onTick()
When the second invocation of bigBang creates a second canvas, this canvas becomes the one for the instance of Keeper in this world. All drawing
actions go there and invocations of update on the above successor of b ensure that all future successors of bthat is, the results of additional calls of
onTicksee this new canvas.
With this explanation in hand, fixing our design is obvious. Every invocation of bigBang must create a new instance of Keeper:
abstract class AWorld {
private Keeper myKeeper;
protected Canvas theCanvas;
public boolean bigBang(int w, int h, double c) {
myKeeper = new Keeper(this);
theCanvas = myKeeper.bigBangC(w,h,c);
return true;
}
...
From then on, this Keeper object is the caretaker of the first instance of (a
subtype of) AWorld and all of its successors created from event handling
methods. Figure 152 shows how it all works. On the left, you see the
complete and correct definition of AWorld and on the right you find the
definition of Keeper. Study them well; they teach valuable lessons.
Exercise
Exercise 27.37 Fix your implementation of AWorld and conduct the experiment of exercise 27.36 again.
Section 28
434
theCanvas =
myKeeper.bigBangC(w,h,c);
return true;
}
void update(Keeper wk, Canvas tc) {
myKeeper = wk;
theCanvas = tc;
void onTick() {
crnt = crnt.onTick();
crnt.update(this,theCanvas);
crnt.draw();
}
boolean endOfTime() {
myKeeper.endOfTime();
return true;
crnt = crnt.onKeyEvent(ke);
crnt.update(this,theCanvas);
crnt.draw();
AWorld endOfWorld(String s) {
myKeeper.endOfTime(s);
return this;
}
}
void draw() {
crnt.draw();
28 Equality
If you paid close attention while reading this chapter, you noticed that the
notion of same comes up rather often and with a subtly different meaning from the one you know. For example, when the discussion of circular
Equality
435
objects insists that following some containment arrow from one field to another in a collection of objects brings you back to the very same object.
Similarly, once assignment is introduced, you also see phrases such as the
world stays the same but some of its attributes change.
Given statements that seem in conflict with what we know about equality, it is time to revisit this notion. We start with a review of extensional
equality, as discussed in section 21, and then study how assignment statements demand a different, refined notion of equality.
Section 28
436
Equality
437
signature for this method are similar to the ones for same:
inside of DroppingBlock :
// is this truly the same DroppingBlock as other?
boolean eq(DroppingBlock other)
The name eq has been chosen for historical purposes.66 The word truly
has been added because were not talking about extensional equality, where
two blocks are the same if their fields are equal:
class IntExamples {
DroppingBlock db1 = new DroppingBlock(1200);
DroppingBlock db2 = new DroppingBlock(1200);
boolean test12 = check db1.eq(db2) expect false;
Then again, comparing an instance of DroppingBlock with itself should definitely yield true:
inside of IntExamples :
boolean test11 = check db1.eq(db1) expect true;
Thus we have two behavioral examples but they dont tell us more than we
know intuitively.
Here is an example that is a bit more enlightening:
inside of IntExamples :
DroppingBlock db3 = db1;
boolean test13 = check db1.eq(db3) expect true;
Giving an instance of DroppingBlock a second name shouldnt have any effect on intensional equality. After all, this happens regularly while a program is evaluated. For example, if your program hands db1 to a constructor
of a World-style class, the parameter of the constructor is a name for the object. Similarly, if you invoke drop on db1, the method refers to the object via
thiswhich is of course just a name for the object.
This last idea plus the motivating sentence it remains the same even
though it changes suggests one more example:
boolean test13again() {
db1.drop();
return check db1.eq(db3) expect true;
}
66 Lisp
Section 28
438
}
First the method must save the current values of the two height fields via
local variables. Second it may assign to them. Third, it must restore the old
connection between fields and values with a second pair of assignments so
that it has no externally visible effects.
Equality
439
The rest requires code for our idea that if it also changes, it is the same.
In more concrete words, if the method assigns a new value to this.height
and other.height changes, then the two methods are the same. Measuring
a change in value means, however, comparing other.height with an int and
knowing what to expect. Since we cant know the value of other.height, lets
just assign a known value to this field, too. Now if this second assignment
affects the this.height again, we are dealing with one and the same object:
inside of DroppingBlock :
boolean eq(DroppingBlock other) {
...
this.height = 0;
other.height = 1;
// this and other are the same if:
. . . (this.height == 1) . . .
...
}
Of course, the method cant return the result yet because it first needs to
restore the values in the fields. Therefore, the result is stored locally.
The gray-shaded area in figure 153 is the complete definition of the eq
method, and it does represent the essence of INTENSIONAL EQUALITY:
if a change to one object affects a second object, you are dealing
with one and the same object.
At the same time, eq is a method that assigns to the fields of two objects, yet
has no visible side-effect because it undoes the change(s) before it returns.
Ponder this observation before you move on.
Exercises
Exercise 28.1 Why can eq not just modify one instance of DroppingBlock via
an assignment? Why does it have to change this.height and other.height?
Develop a test case that demonstrates that a version without one of
the two assignment statements would fail to act like eq. In other words,
it would consider two distinct instances the same, contrary to the spirit of
eq, which only identifies this and other if they were created from one and
the same new expression.
Exercise 28.2 Discuss: Is it possible to design an eq method for a class that
has a single field whose type is some other class? Consider the following
Section 28
440
two hints. First, null is the only value that has all types. Second, calling the
constructor of a random class may have visible and permanent effects such
as the creation of a canvas on your computer screen.
Equality
441
For your convenience, figure 154 repeats the interface for the IItem union of
Coffee and Tea variants. (Also see figures 113 and 114.)
As you can see from this interface, supporting a single same method in a
union demands the addition of two auxiliary methods per variant. Adding
a variant thus requires the implementation of all these methods, plus two
more for each already existing variant. Exercises 21.6 and 21.7 bring home
this conundrum, also demonstrating how abstraction can reduce some of
the work in this case, though not all.
Equipped with null and a fast mechanism for discovering its presence,
we can further reduce the work. Specifically, we can combine the two methods into one because null has all possible types and thus passes the type
system and because == can discover null cheaply. Concretely, we retain
the conversion method and eliminate the method that is like a predicate in
Scheme:
inside of IItem :
// convert this to Coffee (to null otherwise)
Coffee toCoffee();
Defining this method is straightforward. In Coffee, returns this; everywhere
else, say in Tea, it returns null:
Section 28
442
inside of Coffee :
Coffee toCoffee() {
return this;
}
inside of Tea :
Coffee toCoffee() {
return null;
}
Like the original version (on the left), the new version (on the right) converts other to Coffee. If this conversion step produces null, the boolean expression evaluates to false; otherwise, it invokes the same method on the
result of the conversion and hands over this, whose type is Coffee. The resolution of overloading thus resolves this second method call to the private
method (see figure 114) that compares one Coffee with another.
Exercise
Exercise 28.3 Complete the definitions of the Tea and Coffee classes, each
implementing the revised IItem interface. Then add the Chocolate from exercise 21.6 as a third variant to the IItem union. Finally, abstract over the
commonalities in this union.
Equality
443
The gray-shaded box with subscript 1 determines whether other is an instance of the Tea class, i.e., whether it is created with new Tea (or for some
subclass of Tea). The gray-shaded box with subscript 2 is called a CAST. It
tells the type checker to act as if other has type Tea even though its actual
type is IItem, a supertype of Tea. Later when the program runs, the cast
checks that other is indeed an instance of Tea. If it is not, the cast raises
an error, similar to the original conversion method toCoffee in figure 114.67
Fortunately, here we have already confirmed with instanceof that other is a
Tea so nothing bad will happen.
Finally, since testing extensional equality is such a ubiquitous programming task, Java defines a default method and behavior:
inside of Object :
public boolean equals(Object other) {
return this == other;
}
The class Object is the superclass of all classes, and if a class doesnt explicitly extend another class, Java makes it extend Object. Thus, all classes come
with a useless public definition of equals. To obtain mostly useful behavior,
you must publicly override equals and define extensional equality according to your needs. Since all classes provide equals, your method may safely
call equals on all contained objects. For classes that you defined, equals behaves as defined; for others, it may incorrectly return false but at least it
exists.68
Section 28
444
interface IList {
// effect: change the rest of this list
void setRest(IList rst);
Equality
445
To test cyclic lists, you also want to add a method that creates such lists.
Here is one way of doing this, there are many others:
inside of Examples :
// create a cyclic list with one element
IList makeCyclic(int x) {
IList tmp = new Cons(x,new MT());
tmp.setRest(tmp);
return tmp;
}
As you can see, the method creates a list and immediately uses setRest to
set the rest of the list to itself. That is, the assignment statement in setRest
changes tmp just before it is returned.
With this method, you can introduce two obviously equivalent cyclic
sample lists and check whether equals can compare them:
inside of Examples :
boolean test4() {
IList clist1 = makeCyclic(1);
IList clist2 = makeCyclic(1);
return check clist1.equals(clist2) expect true;
}
If you now run the program consisting of this four classes, ProfessorJ does
not stop computing. You must need the STOP button to get a response in
the interactions window.
What just happened? The method makeCyclic is invoked twice. Each
time it creates a list that consists of a single instance of Cons whose rst field
points to the object itself:
fst:
rst:
Since both clist1 and clist2 represent just such lists, we should thus expect
that clist1.equals(clist2) produces true. To understand why it doesnt, lets
step through the computation. According to the law of substitution, the
method invocation evaluates to
ProfessorJ:
Memory Limit
Section 28
446
Equality
447
Exercises
Exercise 28.4 Add the following list to Examples:
fst:
rst:
fst:
rst:
- 1
Compare it with the cyclic list containing one instance of Cons with 1 in
the fst field. Are these two lists the same? If not, design a boolean-valued
method difference that returns true when given the list with two Cons cells
and false when given the list with one Cons cell.
Exercise 28.5 In How to Design Programs, we developed several approaches
to the discovery of cycles in a collection of data. The first one exploits accumulators, i.e., the use of an auxiliary function that keeps track of the pieces
of the data structure it has seen so far. Here is the natural adaptation for
our lists:
inside of IList :
// is this list the same as other, accounting for cycles?
// accumulator: seenThis, the nodes already encountered on this
// accumulator: seenOther, the nodes already encountered on other
boolean equalsAux(List other, IListC seenThis, IListC seenOther);
That is, the auxiliary method equalsAux consumes this plus three additional
arguments: other, seenThis, and seenOther. The latter are lists of Cons nodes
that equalsAux has encountered on its way through this and other.
Design IListC, a data representation for lists of Cons objects. Then design the method equalsAux. While the problem already specifies the accumulator and the knowledge it represents, it is up to you to discover how
to use this knowledge to discover cycles within lists of ints. Finally modify
the definition of equals
inside of IList :
// is this list the same as other
boolean equals(List other);
Section 28
448
in Cons and Mt (as needed) so that it produces true for test4 from above.
Does your method also succeed on these tests:
inside of Examples :
boolean test5() {
List clist1 = new Cons(1,new MT());
List clist2 = new Cons(1,new MT());
inside of Examples :
boolean test6() {
List clist0 = new Cons(1,new MT());
List clist1 = new Cons(1,clist0);
clist1.setRest(clist2);
clist2.setRest(clist1);
return
check clist1.equals(clist2) expect true;
clist0.setRest(clist1);
clist2.setRest(clist3);
return
check clist1.equals(clist2) expect true;
Assignments
Intermezzo 4: Assignments
purpose: the syntax of beginning student that is used in chapter IV
interface, implements, inner classes
449
450
Intermezzo 4
Assignments
TODO
451
Java 5:
Eclipse
Chapter III teaches you how to deal with classes that share fields and contain similar methods. To abstract over those similarities, you create superclasses and lift the common elements there, using inheritance to share them
in many different classes. Like higher-order functions in How to Design Programs, superclass abstraction comes with many advantages.
You should therefore find it disturbing that this technique doesnt work
for the case when everything but the types are shared. For example, we
have seen lists of shots in our case study; you have created lists of worm
segments for the worm game; you have designed data representations for
restaurant menus, which are lists of menu items; and your programs have
manipulated phone books, which are lists of names and phone numbers.
At least to some extent, these lists are the same except that the type of list
element differs from one case to another.
This chapter introduces techniques for abstracting over classes and systems of classes that differ in types not just in fields and methods. In terms
of How to Design Programs (chapter IV), the chapter introduces you to the
mechanisms of abstracting over data definitions; in contrast to Scheme,
where you used English for this purpose, Java offers linguistic constructs
in support of this form of abstraction. Once you know how to abstract over
the types of data representations, you know how to design data libraries;
in the last section of this chapter, we also discuss how to turn libraries into
extensible frameworks.
453
of abstraction over data with the simple case of list entries, demonstrating
the ideas as much as possible before we move on to interesting cases, including lists.
+------------------+
| PhnBEntry
|
+------------------+
| String name
|
| PhoneNumber pnum |
+------------------+
+------------------+
| MenuItem
|
+------------------+
| String name
|
| int price
|
+------------------+
PhnBEntry(String name,
PhoneNumber phnu) {
this.name = name;
this.phnu = phnu;
}
...
MenuItem(String name,
int price) {
this.name = name;
this.price = price;
}
...
454
Section 30
455
Classes without methods are boring. Since phone books and menus
need sorting, lets add the relevant methods to our classes. Sorting assumes
that a program can compare the entries on each list with each other. More
precisely, the representations for entries in a phone book and for items on a
menu must come with a lessThan method, which determines whether one
item is less than some other item:
inside of PhnBEntry :
inside of MenuItem :
// does this entry precede
// does this menu item cost
// the other alphabetically?
// cost less than the other?
boolean lessThan(PhnBEntry other) {
boolean lessThan(MenuItem other) {
return
return
0 > name.compareTo(other.name);
this.price < other.price;
}
}
The two method definitions are straightforward; the only thing worthy of
attention is how to find out that one String is alphabetically less than some
other String.71
71 Search
Section 30
456
While the two lessThan methods in PhnBEntry and MenuItem are distinct
and cant be abstracted, it is nevertheless necessary to check how to design
them if the classes arent designed from scratch but derived from Entry.
Doing so reveals a surprise:
inside of PhnBEntry: (figure 157)
boolean lessThan(PhnBEntry other) {
return
0 > name.compareTo(other.name);
}
The definition in PhnBEntry is identical to the original one; the one for the
derived version of MenuItem, however, is three lines long instead of one.
The additional lines are a symptom of additional complexity that you
must understand. Ideally, you would like to write this:
inside of MenuItem :
boolean lessThan(MenuItem other) {
return this.value < other.value;
}
where value is the name of the second, abstracted field, which replaces price
from the original MenuItem class. Unfortunately, writing this comparison
isnt type-correct. The value field has type Object in MenuItem, and it is
impossible to compare arbitrary Objects with a mathematical comparison
operator. Still, youthe programmerknow from the constructor that the
value field always stands for an integer, not some arbitrary values.72
Castsfirst mentioned in conjunction with Objectbridge the gap between the type system and a programmers knowledge about the program.
Remember that a cast tells the type checker to act as if an expression of one
type has a different type, usually a subtype. Later when the program runs,
the cast checks that the value of the expression is indeed an instance of the
acclaimed type. If it is not, the cast signals an error.
Here we use casts as follows (or with variable declarations as above):
inside of MenuItem :
boolean lessThan(MenuItem other) {
return (Integer)this.value < (Integer)other.value ;
1
2
}
72 As
a matter of fact, you dont really know. Someone derives a stateful subclass from
your class and assigns strange objects to value. See final in your Java documentation.
457
The gray-shaded box labeled with subscript 1 requests that the type checker
uses this.value as an Integer (i.e., an integer) value not an instance of Object,
which is the type of the field-selection expression. The box labeled with
subscript 2 performs the same task for the value field in the other instance
of MenuItem. Once the type checker accepts the two expressions as Integertyped expressions, it also accepts the comparison operator between them.
Recall, however, that the type checker doesnt just trust the programmer.
It inserts code that actually ensures that the value fields stand for integers
here, and if they dont, this additional code stops the program execution.
In summary, the use of Object solves our abstraction problem. It
should leave you with a bad taste, however. It requires the use of a cast to
bridge the gap between the programmers knowledge and what the type
checker can deduce about the program from the type declarations. Worse,
every time lessThan compares one MenuItem with another, the cast checks
that the given value is an integer, even though it definitely is one.
While practical programmers will always know more about their programs than the type portion can express, this particular use of casts is arbitrary. Modern statically typed programming languages include proper
mechanisms for abstracting over differences in types and that is what we
study in the next subsection.
1. empty or
1. empty or
2. (cons IR LoIR).
Section 30
458
To use this definition, we need to supply the name of a class of data, e.g.,
(listof String), or another instantiation of a generic data definition, e.g.,
(listof (list String Number)).
Of course, in How to Design Programs, data definitions are just English
descriptions of the kind of data that a program must process. Java73 and
similar object-oriented languages allow programmers to express such data
definitions as a part of their programs, and the type checkers ensures that
the rest of the program conforms to them.
class PhnBEntry
extends Entry<PhoneNumber> {
PhnBEntry(String name,
PhoneNumber value) {
super(name,value);
}
class MenuItem
extends Entry<Integer> {
MenuItem(String name,
int value) {
super(name,value);
}
459
Section 30
460
461
inside of PhnBEntry :
inside of MenuItem :
boolean lessThan(Entry<PhoneNumber> boolean lessThan(Entry<Integer>
other) {
other) {
return
return
0 > name.compareTo(other.name);
value < other.value;
}
}
Now the signatures of lessThan in the concrete subclasses matches the signature of lessThan in the superclass, and Java accepts the code.
Exercises
Exercise 30.1 Design test cases for the original PhnBEntry and MenuItem
classes. Then revise the classes in figure 158 according to the discussion
and re-run the test cases for the abstracted solution.
Exercise 30.2 Explain why adding
inside of Entry (in figure 157) :
// is this less than the other?
abstract boolean lessThan(Object other);
and making the class abstract doesnt express the same specification as the
extension of Entry<VALUE>. To do so, design concrete subclasses of the
revised Entry class that represent phone-book entries and menu items.
When you define a generic class, you may use as many parameters as
you wish:
class Pair<LEFT,RIGHT> {
LEFT l;
RIGHT r;
Pair(LEFT l, RIGHT r) {
this.l = l;
this.r = r;
}
This class definition says that a Pair consists of two pieces. Since we dont
know the types of the two pieces, the definition uses parameters.
Section 30
462
463
In short, type parameters of generic classes are really just like parameters
of functions and methods.
Exercises
Exercise 30.3 Create instances of Pair<String,Integer> and Posns. Is an instance of Pair<Double,Double> also of an instance of Posn?
Explore why this expression
new Posn(1.0,2.0) instanceof Pair<Double,Double>
is illegal in Java. Note: This part is a challenging research exercise that
requires a serious amount of reading for a full understanding.
Exercise 30.4 Develop a version of Pair that uses Object as the type of its
fields. Then extend the class to obtain a definition for Posn, including its
distanceToO method.
Even though Pair and Entry are trivial examples of abstractions over
similar classes, they teach us a lot about abstracting via Object and generics.
Suppose we had turned Pair into a library class and wanted to add some
basic methods to it, i.e., methods that apply to all instances of Pair no matter
what the concrete field types are. One of these methods is getLeft:
inside of Pair<LEFT,RIGHT> :
// the l field of this Pair
LEFT getLeft() {
return this.l;
}
If l is a private field, getLeft provides access to its current value. Interestingly enough the methods signature communicates to any reader what the
method does. It consumes one argument, this of type Pair<LEFT,RIGHT>
and it returns a value of type LEFTregardless of what these types really
are. Since the given instance contains only one value of type LEFT, the
method can only extract this value.78
A slightly more interesting example is swap, which produces a new pair
with the values reversed:
78 Technically,
the method could also use the null value as something of type LEFT, but
remember that null should only be used in certain situations.
Section 31
464
inside of Pair<LEFT,RIGHT> :
// create a pair with the fields in the reverse order of this
public Pair<RIGHT,LEFT> swap() {
return new Pair<RIGHT,LEFT>(r,l);
}
The method creates a new pair using the values in r and l in this order.
Just as with getLeft, the signature Pair<RIGHT,LEFT> of swap actually reveals a lot about its computation. Given this instance of Pair whose l and
r fields are of type LEFT and RIGHT, respectively, it creates an instance of
Pair<RIGHT,LEFT>. Naturally, without any knowledge about the actual
types, it can only do so by swapping the values of the two types to which
it has guaranteed access.
Exercise
Exercise 30.5 Design (regular) classes for representing lists of MenuItems
and PhnBEntrys. Add methods for sorting the former by price and the latter
by alphabet.
465
IPhnB
MtPhnB
ConsEntry
IMenu
MtMenu
PhnBEntry fst
IPhnB rst
ConsMenu
MenuItem fst
IMenu rst
for the common list interface and the two implementing classes. Even after we agree on some consistent naming scheme, howeversay, IList, Cons,
and Mtwe are left with a difference: PhnBEntry versus MenuItem, the
types of the items on the list.
This situation should remind you of subsection 30.2 where two classes
differed in one type and we abstracted via a common supertype. Or you
could think of subsection 30.3 where the process exploited generic classes.
The big difference is that we are now abstracting in the context of a system
of classes, not just an individual class.
Section 31
466
class Examples {
IList mt = new Mt();
IList
Mt
IList example1 =
new Cons(new Object(),
mt);
Cons
Object fst
IList rst
MenuItem pasta =
new MenuItem("Pasta",12);
MenuItem pizza =
new MenuItem("Pizza",11);
IList example2 =
new Cons(pasta,
new Cons(pizza,
mt));
// a list of Objects
interface IList {}
// an empty list
class Mt implements IList {
Mt() { }
}
IList example3 =
new Cons("hello",
new Cons("world",
mt));
IList example4 =
new Cons(pasta,
new Cons(1,
new Cons("a",
mt)));
5. and last but not least example4 is a list consisting of three objects created from three distinct classes.
These examples show that lists of Objects may be HOMOGENEOUS , containing objects that belong to one type, or HETEROGENEOUS , containing objects
that belong to many different types. Hence, if one of your methods extracts
an item from such a list of Objects, it may assume only that it is an instance
of Object. Using the instance in any other way demands a cast.
Our next step is to add some basic methods to our general representation of lists, just to explore whether it is feasible to turn these classes into a
generally useful library. If so, it is certainly a worthwhile effort to abstract
from the representation to lists of PhnBEntrys and MenuItems.
467
We have turned this wish list into method signatures and purpose statements for our new interface, following our standard method design recipe.
Remember that when there is demand for several methods, you are usually best off developing a general template and then filling in the specific
methods:
inside of Cons :
inside of Mt :
??? meth() {
??? meth() {
return . . .
return . . . this.first . . . this.rest.meth()
}
}
Keep in mind that the expression this.first reminds you of the value in the
first field and the fact that your method can call methods on first.
Filling in those templates for the first two methods is straightforward:
1. count:
inside of Mt :
int count() {
return 0;
}
inside of Cons :
int count() {
return 1 + this.rest.count();
}
inside of Cons :
boolean contains(Object o) {
return this.first.equals(o)
|| this.rest.contains(o);
}
Section 31
468
In this case, the method invokes equals on this.first to find out whether
it is the object in question. Since the Object class defines equals, the
method is available in all classes, even if it may not compute what
you want.
3. asString:
The completion of the definition for asString demands some examples. Say you had to render the following two lists as strings:
IList empty = new Mt();
IList alist = new Cons("hello",new Cons("world",empty));
One possibility is to just concatenate the strings and to use the empty
string for instances of Mt:
checkExpect(empty.asString(),"")
checkExpect(alist.asString(),"helloworld")
This isnt pretty but simple and acceptable, because the goal here isnt
the study of rendering lists as Strings.
As it turns out, the Object class not only defines equals but also the
toString method, which renders the object as a string. Hence, all
classes support toString, because they implicitly extend Object. The
String class overrides it with a method that makes sense for Strings;
you are responsible for the classes that you define. In any case, invoking toString on this. first and concatenating the result with this.
rest.asString() is the proper thing to do:
inside of Mt :
String asString() {
return "";
}
inside of Cons :
String asString() {
return
first.toString()
.concat(rest.asString());
}
Exercises
Exercise 31.1 Use the examples to develop tests for the three methods.
Modify asString to become the toString method of IList.
469
Section 31
470
MtMenu() {
super();
}
471
Exercises
Exercise 31.4 Create a sample list of MenuItems using the implementation
of the data representation in figure 162. Then use the count, contains, and
asString methods from the general list library to develop tests.
Exercise 31.5 Define the representation of phone books as an extension of
the general list library. Define PhoneNumber as a simple stub class and develop tests for count, contains, and asString. Consider reusing those of exercise 31.4. Why is this possible?
}
whose purpose it is to represent functions. It doesnt make any sense to
compare such functions or to sort them.
Put positively, you can only sort lists of objects if the objects also support a comparison method that determines when one object is less than
some other object. In particular, the data definition of figure 160 is not
quite appropriate because it allows all kinds of objects to be stored in a list.
If our list abstraction is to include a sorting method, we must insist that the
members of the list are comparable.
Section 31
472
interface IList {
// sort this list, according to lessThan
IList sort();
// insert o into this (sorted) list
IList insert(IComp o);
}
class Mt implements IList {
Mt() {}
interface IComp {
// is this object less than o?
boolean lessThan(Object o);
}
In Javaand related languagesyou use interfaces to express such requirements. Specifically, the Cons class does not use Object as the type of
first but IComp, an interface that demands that its implementing classes
support a method for comparing and ranking its instances. The top-right of
figure 163 displays this interface; its single method, lessThan, is like equals
in Object, in that it consumes an Object and compares it with this.
Other than the small change from Object to IComp the class definitions
for representing lists and sorting them is straightforward. The interface
and class definitions of figure 163 are basically like those in figure 160. The
473
methods for sorting such lists are straightforward. Their design follows the
standard recipe, producing code that looks just like the one in section 15.2.
class MenuItem implements IComp {
String name;
int value;
MenuItem(String name, int value) {
this.name = name;
this.value = value;
}
Section 31
474
and, if so, casts it to one and compares the values of this and the given
object. Note how the cast is performed via an assignment to a local variable.
Exercises
Exercise 31.6 Complete the definition of lessThan in figure 164. Also design
a class for representing phone book entries so that you can reuse the library
of sortable lists to represent phone books. Use the two classes to represent
menus and phone books.
Exercise 31.7 Is it possible to use Object as the type of inserts second parameter? What would you have to change in figure 163?
Exercise 31.8 Design Apple and Orange classes that represent individual
fruits (weight, ripeness, etc). Both classes should implement IComp where
lessThan returns false if the second input belongs to the wrong kind of class.
Then show that it is possible to compare Apples and Oranges. Is this good
or bad for programming?
Next consider the proposal of changing the type of lessThans second
argument to IComp:
class IComp {
// is this object less than o?
boolean lessThan(IComp other);
}
Specifically ponder the following two questions:
1. Is our original solution in figure 163 more general than this one?
2. Does this definition prevent the comparison of Apples and Oranges?
Conduct coding experiments to find the answers.
Finally, read the sections on Comparable and Comparator in the official
Java documentation.
Problem is, the list library in figure 163 forces you to duplicate code.
Specifically, the library duplicates the functionality of the original list library in figure 160. Because the new Cons class compounds objects of type
IComp, you can no longer use this new library to represent lists of objects
interface IList {
// sort this list, according to lessThan
IList sort();
// insert o into this (sorted) list
IList insert(IComp o);
}
class Mt implements IList {
Mt() {}
public IList sort() {
return this;
}
475
interface IComp {
// is this object less than o?
boolean lessThan(Object o);
}
whose class doesnt implement IComp. Worse, if programmers keep making up list representations for special kinds of objects, say IStringable for
things that can be rendered as Strings or IDrawable for objects that can be
drawn into a Canvas, then this form of duplication quickly proliferates.
If you are in charge of both libraries, a solution is to continue using Object as the type of the list elements. Figure 165 displays the revised library
code. The gray-shaded boxes highlight the changes. In particular, the type
of Conss first field is Object. Since insert is invoked from sort using the value
Section 31
476
in the first field, the argument must be cast from Object to IComp; see the
framed and gray-shaded box. This cast allows the type checker to bless the
use of lessThan on the current first field but also performs a check at runtime that the given Object implements IComp. If it doesnt, the program
signals an error and the evaluation stops. Otherwise evaluation continues
with the method invocation, using the existing object unchanged.
Exercises
Exercise 31.9 Demonstrate that the list library of figure 165 can represent
menus and phone books. Add count, contains, and asString.
Exercise 31.10 Explain the error that occurs when you try to represent a
non-empty list of instances of Add200 using the library in figure 163.
Now use the library in figure 165 to represent the same list. Can you
sort the lists? What kind of error do you encounter now?
477
ITEM
IList
Mt
class Examples {
IList<MenuItem> mt =
new Mt<MenuItem>();
MenuItem pasta =
new MenuItem("Pasta",12);
MenuItem pizza =
new MenuItem("Pizza",11);
IList<MenuItem> menu =
new Cons<MenuItem>(pasta,
new Cons<MenuItem>(pizza,
mt));
Cons
ITEM fst
IList rst
IList<String> los =
new Cons<String>("hello",
new Cons<String>("world",
new Mt<String>()));
class Cons<ITEM>
implements IList<ITEM> {
ITEM first;
IList<ITEM> rest;
IList<Object> loo =
new Cons<Object>("hello",
new Cons<Object>(pasta,
new Mt<Object>());)
then this method must be applied to lists of the exact same type.
The second usage shows up in the implements clauses of Mt and Cons
where IList is applied to the type parameter of the defined class. That is, the
type parameter itself is bound in the position following the class names, i.e.,
in Mt<ITEM> and Cons<ITEM>. Naturally, these type parameters can be
systematically renamed, just like function or method parameters, without
changing the meaning of the interface or class definition. Thus,
Section 31
478
Exercise
Exercise 31.11 Here is a wish list for the generic list implementation:
inside of IList<ITEM> :
// how many objects are on this list?
int count();
479
Section 31
480
// comparable objects
interface ICompG<T> {
// is this object less than other?
boolean lessThan(T other);
}
481
where IComp is a reference to the interface from figure 165. It demands that
you apply IList to subtypes of IComp. Thus, each item that insert consumes
must implement a lessThan method. Similarly,
class Cons <I extends IComp> extends IList<I> {
I first;
IList<I> rest;
Cons(I first, IList<I> rest) {
this.first = first;
this.rest = rest;
}
...
}
is a class definition that extends IList. It does so at type I, which is the type
parameter of the class definition and which is also required to be a subtype
of IComp. The body of the class shows that every value in the first field is of
type I and, due to the constraint on I, any method within Cons can invoke
lessThan on first. Since the lessThan method is applicable to any other Object,
first can now be compared to everything.
In figure 167 you can see three applications of this new syntax, all highlighted in gray. Together they enforce that a list contains only comparable
objects. The first one comes with the IList interface itself; it specifies that its
type parameter must be below some type like IComp, i.e., an interface that
demands comparability. (For now, ignore the actual interface used in the
constraint.) The informal description uses the type parameter to explain
why it exists, and the formal restriction tells us that the list consists of comparable objects. It is possible, however, to implement this specification in a
way that violates the intention behind the purpose statement and the type
parameter.
While the interface does not determine the shape of the list, it does imply two guarantees for all designers who work with IList. First, the designer of every implementation of IList must ensure that the class satisfies
this constraint on the type parameter, too. Second, the designer of every
insert method may assume that the methods second argument implements
the specified interface. As you can see below, these kinds of facts help a lot
when you work with a generic library.
The primary consequence explains the shape of the class headers and
the second and third use of this new syntax. Because these classes are to implement the interfacevia implements IList<I>they must ensure that I,
their type parameter, is a subtype of IComp. The constraints in the type
Section 31
482
483
Exercises
Exercise 31.13 Demonstrate that this parameteric list library can be used to
represent menus and phone books, including their sorting functionality.
Exercise 31.14 Add the count, contains, and asString methods to the generic
list library. What do you expect when you use asString on a list of MenuItems? What do you actually get? Can you improve this?
Exercise 31.15 Exploit the type signatures and constraints on the types to
argue that the result of sort and insert are lists of comparable objects.
80 There is still the problem of null, which may show up at any type; we ignore this for
now.
81 If something does go wrong, it is the fault of the type checking system in your language
and some company will then fix this mistake for all users of the language.
484
Section 32
485
+---+
| T |
+---+
/ \
/
\
/
\
+---+
+---+
| A |
| B |
+---+
+---+
assumption is realistic and exercise 32.6 shows how to get around the assumption
when it doesnt hold.
Section 32
486
that differed in the component types only: MenuItem played the role
of A and PhnBEntry played the role of B.
abstracting over types: Once you understand the similarity, it is time to
eliminate the differences in types with generalized types. To do so,
you may use either subtyping or generics. Because this design recipe
applies to both cases, the specifics of the two approaches can be found
in subsections 32.2 and 32.3. For both approaches, though, it is critical
to inspect CollectA for constraints on A and CollectB for constraints
on B.
The methods and initialization expressions of CollectA (CollectB) may
impose constraints on type A (B) in one of two ways: they may reference a field from A (B) or they may invoke a method on some object
of type A (B). We deal with each case in turn.
Say CollectA and CollectB refer to some field f in an object of type A
and B, respectively. Consider (1) adding a method getF to A and B that
retrieves f s content and (2) replacing references to f in CollectA and
CollectB with invocations of getF. If doing so is impossible (because A
or B are beyond your control), consider abandoning the generalization process. We ignore fields in A and B from now on.84
Say CollectA and CollectB invoke some method m in an object of type
A and B, respectively. In this case, we need to inspect and compare
the method signatures of m in A and B. This comparison produces
constraints that dictate how you generalize:
1. In the first and simplest case, m consumes and produces types
of values that are unrelated to the types under consideration:
inside of A :
int m(String s, boolean b)
inside of B :
int m(String s, boolean b)
Here we can add it to an interface and use it as needed in Collect. Furthermore, this kind of constraint does not necessitate
changes to A or B for the next design step.
2. The second example involves the type under consideration:
inside of A :
A m(String s, boolean b)
inside of B :
B m(String s, boolean b)
84 If you wish to generalize in this situation, you must use a common superclass of A and
B that contains f . This constraint is extremely stringent and hardly ever yields generally
useful libraries.
487
interface IGen<I> {
I m(String s, boolean b);
}
Any class that implements ISub must define a method m that returns either an instance of the class or at least an instance of a
class that implements ISub. With the generic interface IGen your
class specification can be more precise about what it returns. If
class C implements IGen<C>, its method m is guaranteed to
produce an instance of C, just like m in A and B.
3. The third kind of example involves the type under consideration
as a parameter type:
inside of A :
. . . m(String s, A x)
inside of B :
. . . m(String s, B x)
Regardless of the return type, both methods m consume an argument whose type is the class itself. Again, there are two ways
to express the constraints via interfaces:
interface ISub {
. . . m(String s, Object x)
}
interface IGen<I> {
. . . m(String s, I x)
}
Section 32
488
489
typing in this subsection, including a few ideas that you havent encountered yet. While these new ideas arent critical, they make the generalization process convenient.
Recall that in Java a type is either the name of a class or of an interface
(or a primitive type such as int). To declare that some type T is a subtype of
some type S, a programmer uses one of three declarations:
1. class T extends S, meaning class T extends some other class S;
2. class T implements S, meaning T implements an interface S;
3. interface T extends S, meaning T extends some other interface S.
The Java type checker also considers T a subtype of S if T is connected to S
via a series of immediate subtype declarations.
If you just look at the classes in a program, you see a tree-shaped hierarchy, with Object as the top class. A class definition that doesnt specify an
extends clause is an immediate subtype of Object. All others are immediate
subtypes of their specified superclass.
New: While a class can extend only one other class, it can implement
as many interfaces as needed. Hence, a class can have many supertypes,
though all but one are interfaces. Similarly, an interface can extend as many
other interfaces as needed. Thus, an interface can have many supertypes
and all of them are interfaces.
Section 32
490
than classes. Computer scientists and mathematicians call this kind of hierarchy a DIRECTED ACYCLIC GRAPH (DAG). The important point for us is
that the DAG sits on top and to the side of the tree-shaped class hierarchy.
Figure 169 summarizes the subtyping hierarchy schematically.
Subtyping in Java implies slightly different consequences for classes
and interfaces:
see whether this kind of
inheritance can be done
earlier, then the N EW in 1
and 2 can be dropped
1. A class C that extends class D inherits all visible methods, fields, and
implements declarations.
New: A subclass may override an inherited method with a method
definition that has a signature that is like the original one except that
the return type may be a subtype of the original return type.
Example: Here mmm in C overrides mmm in D:
class D {
. . . D mmm(String s) . . .
}
class C extends D {
. . . C mmm(String s) . . .
}
class C implements I {
. . . public C mmm(String s) . . .
}
491
choosing a replacement type for A and B: The very first step is to choose
a type with which to replace A and B in CollectA and CollectB, respectively. If you wish to stick to subtyping, you have three alternatives
though only two matter:
1. If both A and B are classes, consider using Object as the replacement type. This choice allows the most flexibility and works fine
as long as there are no constraints.
Example: For Pair, a representation of pairs of objects, it is natural
to choose Object as the type for the two fields. There arent any
constraints on the components of a pairing.
2. If both A and B are classes, you might also consider an existing
superclass of A and B. This tends to tie the general Collect class
to a specific project, however, making it unsuitable for a general
library. We therefore ignore this possibility.
3. If either A or B is an interface and some of their method signatures are constraints, you have no choice; you must use an interface to generalize an interface. Similarly, if CollectA and CollectB
make assumptions about methods in classes A and B, you are
better of choosing an interface than Object.
Example: For sortable lists, we tried both alternatives: Object and
an interface. The latter was necessary for the creation of sortable
lists because sorting assumes that the objects on the list implement a lessThan method.
It is possible to choose solutions that mix elements of these three alternatives, but we leave this kind of design to experienced developers.
designing Collect: Once you have decided which replacement type T you
want to use for A and B, replace the designated parts of CollectA and
CollectB with T. If you also rename the two classes to Collect, you
should have two identical classes now.
Unfortunately, these classes may not function yet. Remember that
the methods and initialization expressions of CollectA and CollectB
may impose conditions on T. If you collected all those constraints in
a single interface and if T is this interface, you are done.
If, however, you chose Object to generalize A and B, you must inspect
all method definitions. For each method definition that imposes a
constraint, you must use casts to an appropriate interface to ensure
that the objects stored in Collect implement the proper methods.
Section 32
492
interface IConstraints {
. . . m(String s, Object x)
}
493
inside of Collect :
Object m(String s)
inside of Collect :
void m(Object x)
Section 32
494
495
Section 32
496
Exercises
Exercise 32.1 Create a test suite for CellItem. Then ensure that NewCellItem
passes this test suite, too.
Exercise 32.2 Use Cell to derive a class that manages access to Strings.
Exercise 32.3 Modify Cell so that it also counts all invocations of get, not
just set. This exercises shows again that abstraction creates single points of
control where you can enhance, fix or improve some functionality with one
modification.
The case changes significantly if we add the following to CellItem:
inside of CellItem :
private int weight = 0;
// set the content of this cell to c
public void set(Item content) {
counter = counter + 1;
weight = weight + content.weight() ;
this.content = content;
}
497
Section 32
498
interface ICell {
int weight();
}
class CellObject {
Object content;
private int weight = 0;
...
public void set(Object c) {
ICell content = (ICell)c;
counter = counter + 1;
weight = weight + content.weight();
this.content = content;
}
...
}
class CellIntf {
private ICell content;
private int weight = 0;
...
public void set( ICell content) {
counter = counter + 1;
weight = weight + content.weight();
this.content = content;
}
...
}
499
choice isnt as obvious as this and you may wish to weigh the advantages
and disadvantages of the alternatives before you commit to one.
When a solution like CellObject is the preferred one, you are accepting
additional casts in your class. Each method that makes special assumption
about the objects must use casts. Casts, however, come with three distinct
disadvantages. First, the presence of casts complicates the program. The
conscientious programmer who adds these casts must construct an argument why these casts are proper. That is, there is a reason why it is acceptable to consider an object as something of type A even though its declared
type is Object (or something else). Unfortunately, Java doesnt allow the
programmer to write down this reason. If it did, the type checker could
possibly validate the reasoning, and better yet, any programmer who edits
the program later would be able to re-use and re-check this reasoning.
Second, casts turn into checks during program evaluation. They perform the equivalent of an instanceof check, ensuring that the object belongs
to the specified class or implements the specified interface. If it doesnt, the
type checker was cheated and many other assumptions may fail; hence,
the evaluator raises an exception in such cases. Although such tests arent
expensive, the problem is that they can fail.
Third, when casts fail, they signal an error and stop the program evaluation. As long as it is youthe programmerwho has to study and understand the error message, this isnt much of a problem. If it is your companys most important client, you are in trouble. After all, this customer
typically has no computer training and just wants a functioning program
for managing menus across a chain of a hundred restaurants.
In short, a failure after delivery is costly and is to be avoided if possible. Conversely, eliminating such potential cast failures should be our goal
whenever it is cost-effective to do so. And the desire to eliminate casts is
precisely why Java and other typed object-oriented languages are incorporating generics.
Exercises
Exercise 32.4 Complete the definition of CellIntf in figure 171, including a
test suite. Then consider this class:
class Package {
int postage;
int value;
Section 32
500
501
like applying a function to values. The difference is that the generics computation takes place at the level of types and the function application takes
place in the realm of values. Put differently, it is the type checker that determines the outcome of applying a generic type to a type while it is the
evaluator that determines the value of a function application.
When you use generics to generalize some classes and interfaces into a
framework, you do not think in terms of generalizing types but in terms
of types as parameters. In a sense, generalizing with generics is much like
generalizing with functions in How to Design Programs:
specifying the parameter for Collect: If you choose to use generics to generalize CollectA and CollectB, you pick a type parameter per pair of
corresponding differences. Our running abstract example assumes
that there is one pair of such differences: CollectA uses A where CollectB uses B. Hence, we need one type parameter, say I. For realistic examples, you should consider choosing suggestive parameter
names so that people who read your program get an idea of what is
stored in Collect.
Assuming you have gathered all the constraints on A and B as method
signatures in the interface IConstraints. Recall that constraints are occasionally self-referential, in which case IConstraints itself is parameterized. This suggests using something like the following three forms
as the header of Collect:
1. class Collect <I> if there are no constraints;
2. class Collect <I extends IConstraints> if the constraints are not
self-referential;
3. class Collect <I extends IConstraints<I>> if the constraints refer
to themselves. Keep in mind that the first occurrence of I is the
binding instance, and the second one is an argument to which
the generic interface IConstraints is applied.
Things may get more complicated when several type parameters are
involved, and you may benefit from experimentation in such cases.
Example: The generalization of MenuItem and PhnBEntry needs one
type parameter because the (core of the) two classes differ only in the
type of one field. In contrast, the definition of Pair uses two parameters. Finally, IList<I extends ICompG<I>> is our canonical illustration of a Collect-like class with a constrained interface.
Section 32
502
designing Collect: Now that you have the header of Collect, you just replace A and B with I, respectively. Doing so for CollectA or CollectB,
you should end up with the same class. Since I is specified to be a
subtype of the interface of all constraints, this step is done.
preparing A and B for re-use: Reusing type A for the re-definition of CollectA requires that A implements all the constraints:
1. If there arent any constraints, you dont need to modify A.
2. If the constraints are ordinary, add extends IConstraints or implements IConstraints to As header; the former is for classes, the
latter for interfaces.
3. If the constraints are self-referential, IConstraints is generic and
to obtain a type for an extends or implements clause requires
an application of IConstraints to a type. Naturally, this type is
A itself, i.e., you add implements IConstraints<A> or extends
IConstraints<A> to the header.
With generics, adding these clauses ought to work without further modifications to A. The reason is that generic constraint interfaces capture constraints more precisely than subtyping interfaces.
Example: Consider the following constraint originating in A:
class A {
...
m(String s, A x) { . . . }
...
}
Formulated as an interface, it becomes
interface IConstraints<I> {
...
m(String s, I x) { . . . }
...
}
By applying IConstraints to A in the implements clause, you get
the exact same signature back from which you started:
503
class NewCollectB
extends Collect<B> { . . . }
Section 32
504
class Cell<I> {
private I content;
private int counter = 0;
public I get() {
return content;
}
public I get() {
return content;
}
interface ICell {
int weight();
}
class CellItem extends Cell<Item> {
public CellItem(Item content) {
super(content);
}
}
objects collectively, programs often need other views of collections of objects. Among those are unordered collections or sets of objects; queues of
505
interface ISet<Element> {
// is this set empty?
boolean isEmpty();
// is e a member of this set?
boolean in(Element e);
// create a set from this set and x
Set add(Element x);
// is this a subset of s?
boolean subset(Set<Element> x);
// number of elements in this set
int size()
}
The interface on the left assumes that the elements of a set are Objects; the
one on the right is parameterized over the type of its elements. Design
classes that implement these interfaces, using lists to keep track of the sets
elements. Extend and modify your chosen list libraries as needed.
Exercise 32.9 If you organize your kitchen, it is likely that you stack your
plates and bowls and possibly other things. Designing a data representation for a collection that represents such a form of information is mostly
about designing methods that access pieces in an appropriate manner. Here
are two interface-based specifications of stacks:
506
interface IStack {
// is this stack empty?
boolean isEmpty();
// put x on the top of this stack
void push(Object x);
// remove the top from this stack
void pop();
// the first item on this stack
Object top();
// number of items on this stack
int depth();
}
Section 32
interface IStack<Item> {
// is this stack empty?
boolean isEmpty();
// put x on the top of this stack
void push(Item x);
// remove the top from this stack
void pop();
// the first item on this stack?
Item top();
// number of items on this stack
int depth();
}
According to them, a stack is a bunch of objects to which you can add another object at the top; from which you can remove only the top-most object; and whose top-most object you may inspect. For completeness, our
specifications also include a method for counting the number of items that
are stacked up. Design classes that implement these interfaces. Extend
your list libraries as needed.
Exercise 32.10 Imagine yourself designing a program for your local public
transportation organization that models bus stops. At each bus stop, people can join the queue of waiting people; when a bus shows up, the people
who have waited longest and are at the front of the queue enter the bus.
You may also wish to know who is at the front and how long the queue is.
The following two interfaces translate this scenario into two interfaces
for general queue representations:
interface IQueue {
interface IQueue<Item> {
// is this queue empty?
// is this queue empty?
boolean isEmpty();
boolean isEmpty();
// add x to the end of this queue
// add x to the end of this queue
void enq(Object x);
void enq(Item x);
// remove the front from this queue // remove the front from this queue
void deq();
void deq();
// the first item in this queue
// the first item in this queue
Object front();
Item front();
// number of items in this queue
// number of items in this queue
int length();
int length();
}
}
507
As always, the specification based on subtyping is on the left, the one based
on generics is on the right. Design classes that implement these interfaces.
Modify and extend your chosen list libraries as needed. Describe at least
two more scenarios where a general queue library may be useful.
Exercise 32.11 Consolidate the two list libraries that you developed in exercises 32.8 through 32.10. Then ensure that your classes for representing
sets, stacks, and queues can use one and the same list library.
This last exercise illustrates how generally useful libraries come about.
Programmers turn a generalized data representation into a library, use the
library in many different contexts, and improve/add methods. Eventually
someone standardizes the library for everyone else.86 The next few exercises are dedicated to tree representations, a different but also widely used
collection class and library.
Exercises
Exercise 32.12 Lists are just one way of compounding data. Trees are another one, and they are popular, too. In some cases, organizing data in the
shape of a tree is just the best match for the information; examples include
family trees and representations of river systems. In other cases, organizing
data in this shape is useful for performance purposes.
Design a generalized tree representation for binary trees of objects using
both generics and subtyping. A binary tree is either an empty tree or a
branch node that combines information with two binary trees, called left
and right. Also design the method in, which determines whether some
given object is in the tree.
Exercise 32.13 As you may recall from How to Design Programs (chapter III),
binary search trees are even better than plain binary trees, as long as the
objects in the tree are comparable to each other.
Design a generalized tree representation for binary search trees of comparable objects using both generics and subtyping. A binary search tree is
either an empty tree or a node that combines two binary search trees. More
precisely, a node combines a comparable object o with two binary trees, l
and r; all objects that are less than o occur in l and all those that are greater
86 In the case of Java, Joshua Bloch was Suns designated library designer.
He has distilled
Section 32
508
than (or equal to) o occur in r. Optional: If the object is in the tree, the tree
remains the same. Use privacy specifications to ensure that the condition
for branches always holds.
Also design the method in, which determines whether some given object occurs in this binary search tree.
Exercise 32.14 Design the class SetTree, which implements ISet from exercise 32.8 with binary search trees as the underlying collection class. For
the latter, start from the data representation designed in exercise 32.13 and
modify it as needed. Also design a generic set representation that implements ISet<Element> from exercise 32.8.
For both cases, document where you impose the constraint that this
kind of set representation works only if the elements are comparable.
Before you move on, take a look at the sections on Set and TreeSet in the
official Java documentation.
509
displays the string, plus some optional information about the state of the
evaluation and where the throw expression is located in the program. The
specifics depend on the Java implementation.
Here is a use of RuntimeExceptions for an example from this chapter:
inside of MenuItem :
public boolean lessThan(Object o) {
MenuItem m;
if (o instanceof MenuItem) {
m = (MenuItem)o;
return this.value < m.value; }
else {
throw new RuntimeException("incomparable with MenuItem") ; }
}
At the time when we first designed lessThan for MenuItem (see page 473),
we went with false as the value to return when an instance of some incomparable class was given. Now that we once again have the option of
signaling an error, we should do so.
Several of the exercises in the preceding section call for signaling errors,
too. Consider the pop and top methods in exercise 32.9:
inside of IStack :
// remove the top from this stack
void pop();
// the first item on this stack?
Object top();
Since an empty stack has no elements, it is impossible to retrieve the first
and to remove it from the stack. If you solved the exercise, your method
definitions signal errors that are about empty lists. Naturally, this is unhelpful to a friend or colleague who wishes to use the stack and has no
desire to know how you designed the internals of your Stack class.
In How to Design Programs, you learned how to design checked functions. Now you need to learn how to add checks to methods. We recommend two additions: first, the purpose statement should warn the future
users of IStack that popping or topping an empty stack causes an error; second, the method body should check for this situation and signal an appropriate error:
Section 32
510
inside of IStack :
// remove the top from this stack
// ASSUMES: the stack is not empty: !(this.isEmpty())
void pop();
inside of Stack implements IStack :
public void pop() {
// contract check
if (this.isEmpty())
throw new RuntimeException("pop: stack is empty");
// end
...
}
Note the ASSUMES in the first fragment, making it clear that in addition to the signature, the method needs to enforce additional constraints
on its input. Also note how in the method itself, the checking is visually
separated from the method body proper via visual markers. Following
the practice from How to Design Programs, we call these checks contract
checks.87
Exercises
Exercise 32.15 Equip the sort method from figure 163 with an explicit check
that ensures that the items on the list implement IComp. The goal is to signal
an error message that re-states the assumption of the sort method.
can we checkExpect for
runtime exceptions?
Exercise 32.16 Complete the addition of checks to your stack class from
exercise 32.9.
Exercise 32.17 Inspect the method specifications in the IQueue interfaces of
exercise 32.10 for operations that cannot always succeed. Equip the purpose statements with assumptions and the method definitions with appropriate checks.
Exercise 32.18 Inspect the uses of lists of shots and charges in your War
of the Worlds program from section 27.7. If possible, create a general list
library of moving objects. If not, explain the obstacle.
511
Section 33
512
+----------------------------+
| IList<Item extends IComp> |<--------------------+
+----------------------------+
|
+----------------------------+
|
| IList<Item> sort()
|
|
+----------------------------+
|
|
|
/ \
|
--|
|
|
+----------------------------+
|
| AList<Item extends IComp> |
|
+----------------------------+
|
+----------------------------+
|
| AList<Item> insert(Item i) |
|
+----------------------------+
|
|
|
/ \
|
--|
|
|
----------------------------------|
|
|
|
+-------------------------+
+--------------------------+
|
| Mt<Item extends IComp> |
| Cons<Item extends IComp> |
|
+-------------------------+
+--------------------------+
|
| Object first
|
|
| IList rest
|---+
+--------------------------+
There is no question that sort should be a part of the IList interface, but the
inclusion of insert is dubious. Its purpose statement clearly demonstrates
that it is only intended in special situations, that is, for the addition of an
item to a sorted list. The insert method is at the core of the insertion sort
idea, and its presence in the common interface thus reveals how sort works.
Although making insert available to everyone looks innocent enough, it is
improper. Ideally, the interface should contain just the header of the sort
method and nothing else (as far as sorting is concerned).
Overcoming this problem requires a different organization, but one that
you are already familiar with. Take a look at the diagram in figure 173. It
should remind you of the chapter on abstracting with classes. Like the diagrams there, it includes an abstract class between the IList interface and its
implementing classes. This time, however, the primary purpose of the abstract class is not to abstract some common methods from the implementing classesthough it may be useful for this purpose, too. Its purpose is
to hide the auxiliary insert method from the general audience. Specifically,
the interface IList contains only sort now. The insert method header is in
AList; furthermore, it is unlabeled, indicating that only library code may
refer to it. Since everyone else will use only the interface and the public
class constructors to deal with lists, insert is inaccessible to outsiders.
The translation into classes and interfaces appears in figure 174. It is
513
AList<Item> insert(Item i) {
AList<Item r = new Mt<Item>();
return new Cons<Item>(i,r);
}
AList<Item> insert(Item i) {
AList<Item> r;
if (i.lessThan(first)) {
return
new Cons<Item>(i,this); }
else {
r = (AList<Item>)rest ;
2
return
new Cons<Item>(first,r.insert(i)); }
}
Section 33
514
straightforward, though requires the two gray-shaded casts in Cons. Surprisingly, each cast is due to a different type problem. The one labeled with
subscript 1 tells the type system that the value of rest.sort() can be accepted
as an AList<Item>, even though its type is IList<Item> according to the
signature of sort. The cast labeled with 2 is necessary because the rest field
is of type IList<Item> yet this type doesnt support the insert method.
+----------------------------+
| IList<Item extends IComp> |
+----------------------------+
+----------------------------+
| IList<Item> sort()
|
+----------------------------+
|
/ \
--|
+----------------------------+
| AList<Item extends IComp> |<--------------------+
+----------------------------+
|
+----------------------------+
|
| AList<Item> sort()
|
|
| AList<Item> insert(Item i) |
|
+----------------------------+
|
|
|
/ \
|
--|
|
|
----------------------------------|
|
|
|
+-------------------------+
+--------------------------+
|
| Mt<Item extends IComp> |
| Cons<Item extends IComp> |
|
+-------------------------+
+--------------------------+
|
| Object first
|
|
| AList rest
|---+
+--------------------------+
515
}
class Mt<Item extends IComp>
extends AList<Item> {
public Mt() {}
public AList<Item> sort() {
return this;
}
AList<Item> insert(Item i) {
IList<Item> r = new Mt<Item>();
return new Cons<Item>(i,r);
}
AList<Item> insert(Item i) {
if (i.lessThan(first))
return
new Cons<Item>(i,this);
else
return
new Cons<Item>(first,rest.insert(i));
}
of type AList<Item>. Put differently, the cast ensures that every class that
implements IList<Item> also extends AList<Item>a fact that the library
designer must now keep in mind for any future modifications (or extensions).
Section 33
516
Exercises
Exercise 33.1 Design an example class that tests the sorting algorithm. This
class represents a use of the list library by someone who doesnt know how
it truly functions.
Exercise 33.2 The definition of insert in Cons of figure 176 uses the constructor with an Item and an AList<Item>. By subtyping, this second expression
is typed as IList<Item>. In the constructor, however, this value is again
exposed to a cast. Suppose casts were expensive. How could you avoid the
cast in this particular case? Does your solution avoid any other casts?
Exercise 33.3 Design a list library that is based on subtyping (as opposed
to generics), supports sorting, and hides all auxiliary operations.
Exercise 33.4 Design a generic list representation that implements the IList
interface from figure 176 but uses the quicksort algorithm to implement
sort. You may add append to the interface, a method that adds some give
list to the end of this list. Hide all auxiliary methods via an abstract class.
From the chapter on generative recursion in How to Design Programs,
recall that quicksort uses generative recursion for non-empty lists L:
1. pick an item P from L, dubbed pivot;
2. create a sorted list from all items on L that are strictly less than P;
3. create a sorted list from all items on L that are strictly greater than P;
4. append the list from item 2, followed by the list of all items that are
equal to P, followed by the list from item 3.
Use the example class from exercise 33.1 to test the sorting functionality of
your new list library.
After you have designed the library, contemplate the following two
questions: (1) Does it help to override sort in the abstract class? (2) Does
it help to override append in the abstract class?
517
Section 33
518
This suggest the addition of a method to IList and the addition of an ASSUMES clause to the purpose statement of getFirst:
inside of IList<Item . . . > :
// is this list empty?
boolean isEmpty();
// retrieve the first item from this list
// ASSUMES: this list isnt empty: !(this.isEmpty())
Item getFirst();
The isEmpty method returns true for instances of Mt and false for instances
of Cons. It is an example of a PREDICATE.
Finally, on rare occasions you may also wish to grant the rest of the
program the privilege to modify the content of a field. To do that, you add
a SETTER method such as setFirst:
inside of IList<Item . . . > :
// effect: changes what the first item for this list is
// ASSUMES: this list isnt empty: !(this.isEmpty())
void setFirst(Item newValue);
Providing setters also has the advantage that your library can inspect the
new value for a field before you commit to a change. That is, if values of a
field must satisfy conditions that cant be expressed via the type system, a
setter method can check whether the new value is proper for the field and
signal an error if it isnt.
Exercises
Exercise 33.5 Design a stack library that provides the following:
1. the IStack interface of figure 177 and
2. a constructor for empty stacks that is invoked like this: MtStack().
Here is an Examples class, representing a use of the library:
519
interface IStack<Item> {
// push i onto this stack
IStack<Item> push(Item i);
// is this stack empty?
boolean isEmpty();
// what is the top item on this stack?
// ASSUMES: this isnt the empty stack: !(this.isEmpty())
Item top();
// create a stack by removing the top item from this stack
// ASSUMES: this isnt the empty stack: !(this.isEmpty())
IStack<Item> pop();
}
...
}
Finally add a setter that changes what the top of a stack is.
Exercise 33.6 Design a queue library that provides the following:
1. the IQueue interface of figure 178 and
2. a constructor for empty queues that is invoked like this: MtQueue().
As you design the library, dont forget to design a full-fledged examples
class.
Design the front and deq methods so that they use auxiliary methods.
(Hint: These methods should use an accumulator parameter.) Naturally,
Section 33
520
interface IQueue<Item> {
// add i to the end of this queue
IQueue<Item> enq(Item i);
// is this queue empty?
boolean isEmpty();
// what is at the front of this queue?
Item front();
// remove the front item from this queue
IQueue<Item> deq();
you should hide those auxiliary methods using the technique learned in
section 33.1.
521
interface IPred {
// does this object satisfy
// a certain property?
boolean hasProperty();
}
Now you add select to Cons and Mt, and you make sure that MenuItem and
PhnBEntry implement IPred in an appropriate manner. If this is still a bit
fast for you, study the following exercise.
Exercise
Exercise 33.7 Design a representation for restaurant menus. A menu consists of a series of menu items; each menu item names the food and specifies
a price. Include a method for extracting those items from a menu that cost
between $10 and $20; the result is a menu, too.
Design a representation for phone books. A phone book consists of a
series of entries; each entry names a friend and phone number (use plain
numbers for this exercise). Include a method for extracting those entries
from a phone book that list a friend whose name starts with A; for simplicity, the result is a phone book, too.When you have completed the design,
suggest an alternative form of data for the result of the selection method.
Abstract over the two forms of lists you have designed, including the
method for selecting sub-lists. Use both a subtyping approach as well as a
generic approach. Dont forget to demonstrate that you can still represent
menus and phone books. This should produce a list representation that
Section 33
522
implements the above IList<Item extends IPred> interface and whose items
are instances of a class that implements IPred.
Our true problem is that we now have two list libraries: one that supports sorting and one that supports selecting. If you decide to use the former to represent menus, you need to add your own methods for selecting
menu items in the desired price range. If you decide to use the latter for
phone books, you need to design your own sorting methods. Of course,
what we really want is a list library that supports both methods. Designing
such a library demands a choice, and the choices shed light on the role of
interfaces as we have used them in this chapter.
The first choice is to design a list library whose elements are both comparable to each other and can be inspected for a certain property. Continuing with our generic approach, this demands the formulation of an interface that expresses these two constraints. There are two ways to do so:
interface ICompPred {
boolean lessThan(Object o);
boolean hasProperty();
}
interface ICompPred
extends IComp, IPred { }
The interface on the left includes both a lessThan and a hasProperty method
signature. The interface definition on the right introduces a slightly new
notation, namely an extends clause with comma-separated interfaces. Its
meaning is that ICompPred simultaneously extends both interfaces and inherits their method signatures. Using this form of an extends clause also
makes ICompPred a subtype of IComp as well as IPred; see section 32.1 and
figure 169.
Once we have ICompPred, we can introduce the interface that defines a
generalized list structure:89
interface IList<Item extends ICompPred> {
// sort this list according to lessThan in ICompPred
IList<Item> sort()
// extract the objects from this list
// that satisfy hasProperty from ICompPred
IList<Item> select();
}
89 Java also supports a short-hand for specifying such an interface without introducing
ICompPred: interface IList<I extends IComp & IPred>. This notation exists for convenience,
though, and is otherwise of no interest to program design.
523
All you have to do now is add Mt and Cons classes that implement this
interface and you have a powerful list library. Since this step in the design
is routine, we leave it to the exercises.
Exercises
Exercise 33.8 Finish the design of this list library using generics. Use the
library to represent the menus and phone books from exercise 33.7.
Exercise 33.9 Design a list library like the one in exercise 33.8 using subtyping and interfaces. Use the library to represent the menus and phone
books from exercise 33.7.
If you solved the preceding exercises or if you thought through the rest
of the library design, you understand that this kind of design comes with
a serious drawback. Suppose your library exports the interfaces IList and
ICompPred as well as the constructors Cons and Mt. You can use the constructors directly to represent some list of objects, or you can derive specialpurpose interfaces and classes and then use those to make up a list. In either case, you are forced to design classes of list items that implement the
ICompPred interface. That is, the object on such a list must define a lessThan
and a hasProperty method. It is impossible to create a list of objects that are
only comparable or only inspectable for a property.
In order to represent lists that can compound all kinds of objects, we
need to retort to casts again. Specifically, we need to rewrite the list interface to allow arbitrary items:
interface IList<Item> {
// sort this list according to lessThan in IComp
// ASSUMES: the objects implement IComp
IList<Item> sort()
// extract the objects from this list
// that satisfy hasProperty from IPred
// ASSUMES: the objects implement IPred
IList<Item> select();
}
With an interface like that, your sort and select methods check during program execution whether the items on the list implement the IComp or IPred
interface, respectively. If they dont, the library raises an error.
Section 33
524
interface IPred {
// does this object satisfy the property?
boolean hasProperty();
}
interface IComp {
// is this object less than o?
boolean lessThan(Object o);
}
interface IList<Item> {
// sort this list according to lessThan in IComp
// ASSUMES: the objects implement IComp
IList<Item> sort()
// extract the objects from this list that satisfy hasProperty from IPred
// ASSUMES: the objects implement IPred
IList<Item> select();
}
class Mt<Item>
implements IList<Item> {
...
Mt<Item>() { . . . }
...
}
class Cons<Item>
implements IList<Item> {
...
Cons<Item>(Item i, IList<Item> l) { . . . }
...
}
class PlainItem {
...
}
Figure 179: A flexible list library and four kinds of list items
525
The top half of figure 179 sketches what this library design looks like to
the rest of the world. This version of the list library exports three interfaces
(IList, IPred, and IComp) plus two constructors (Mt, Cons). The bottom half
sketches four classes whose instances may occur in an instance of IList:
1. PlainItem is a class that doesnt implement any interface. While you
can create a list from its instances, you cannot design methods that
sort those lists and you cant have methods that select sub-lists from
them, either.
2. CompItem implements the IComp interface. Hence, a list that consists
of CompItem objects is sortable.
3. Similarly, PredItem implements the IPred interface. Hence, you may
design methods that extract lists with certain properties from lists of
PredItem objects.
4. Lastly, you may design classes that implement two (and more) interfaces. AllItem is a class that implements both IComp and IPred, and it
is thus possible to have methods that sort lists of AllItems as well as
methods that select sub-lists.
The key to the last class definition, AllItem, is yet another small extension
to our notation:
class AllItem implements IComp, IPred { . . . }
As you can see, the implements clause lists two interfaces separated by
commas. The meaning is obvious, i.e., the class must define all methods
specified in all the interfaces that it implements. This notational extension
is similar to the one for extends for interfaces; again, see section 32.1 and
figure 169 for the general idea of extends and implements with multiple
interfaces.
Exercises
Exercise 33.10 Finish the design of this list library using generics. Use the
library to represent the menus and phone books from exercise 33.7.
Exercise 33.11 Design a list library like the one in exercise 33.10 using subtyping. Use the library to represent the menus and phone books from exercise 33.7.
Section 34
526
Given the analysis of this scenario and the preceding chapters, there
appears to be a design tension. In the preceding chapters, we have made
interfaces large, adding as many methods as we wished to provide as much
functionality as needed (for any given problem) for a data representation.
In this chapter, and especially in this subsection, our design analysis suggests making interfaces small, adding just as many method specifications
as needed to let other designersusers of our librariesknow what their
classes must implement.
A close look resolves this tension easily, producing a simple guideline:
G UIDELINE
ON I NTERFACES
527
2. What should you do if you wish to use a library that you cant change
and that doesnt quite deal with all the data variants needed?
The questions look strange, considering that in the past we just edited
classes and added whatever we needed. In reality, however, programmers
are handed libraries and are told not to edit them90 and then they are faced
with just these questions.
+-----------+
*
| IIfc
|
* DATA EXTENSION
+-----------+
*
|
*
/ \
*
--*
|
*
+-------------------+---------------------+---------------------+-------*-------+
|
|
|
|
|
*
|
+-----------+
+-----------+
+-----------+ * +-----------+
|
| ACls
|
| BCls
|
| CCls
| * | DCls
|
|
+-----------+
+-----------+
+-----------+ * +-----------+
|
|
|
|
|
*
====|=======================================================================================
| FUNCTION EXTENSION|
|
|
|
*
|
|
|
|
|
*
+-----------+ /|
|
|
|
|
*
| IExt
|- |----+--|-------------------+-|------------------+--------------+ |
+-----------+ \|
| |
| |
| |
|
|
*
+-----------+
+-----------+
+-----------+ * +-----------+
| AxCls
|
| BxCls
|
| CxCls
| * | DxCls
|
+-----------+
+-----------+
+-----------+ * +-----------+
general, libraries dont come in a readable, textual form; even if they do, development conventions ensure that edits are ignored in the overall product.
Section 34
528
the library, the new class implements the general interface. Finally, in the
bottom right corner you see a piece of code that uses this data extension of
the library and adds functionality to the overall library, too.
In short, you often dont just want libraries; you want extensible frameworks. One reason object-oriented programming languages became popular is because they promised a solution to just this kinds of programming
problems. In this section we investigate this claim and how to approach
the design of extensible frameworks.91
a full understanding of the solution and its implications, you will need to acquire
knowledge about object-oriented program design that is beyond the scope of this book.
529
// a list of integers
interface IList {
// how many objects are on this list?
int count();
// is the given object o on this list?
boolean contains(int o);
// render this list as a string
String asString();
}
class Mt implements IList {
Mt() {}
Exercises
Section 34
530
Exercise 34.1 Develop examples and tests for all methods of figure 181. Extend the test class to cope with the library extension in figure 182.
Exercise 34.2 Design a sort method for the library of figure 181.
Then add a sort method to the library extension of figure 182 without
modifying the core library. Hint: To solve this problem, you will need to
design a method that inserts each element in a range into the sorted list. As
you do so, remember the lessons concerning the design of functions that
process natural numbers. Also see section 34.4.
531
The second exercise indicates how a data extension may suggest a function extension as well. A moments thought shows that the extension of the
union with a Range variant demands the addition of a method that inserts
a range into a list. We havent covered function extensions, however; so
doing so has to wait.
Section 34
532
Figure 183: Deriving sortable lists from Object lists, the class diagram
of the type checker, rest has type IList.92 Thus if you wrote
rest.select()
the type checker would fail. Instead, the method uses local variable definitions to cast rest to IMenu and first to MenuItem. The rest of the method
body refers to these local variables instead of the fields.
Exercises
92 Remember
533
Exercise 34.3 Define MtMenu in analogy to ConsMenu for the library in figure 184. Also design a MenuItem class that represents item on a menu and
supports the method hasProperty. Finally, develop examples and tests for
IMenu in figure 184.
Section 34
534
inside of MenuExamples :
MenuItem fish = new MenuItem("Fish & Chips",1295);
MenuItem steak = new MenuItem("Steak & Fries",2095);
MenuItem veggie = new MenuItem("French Veggies",1095);
IMenu mt = new MtMenu();
IMenu m1 = new ConsMenu(fish,mt);
IMenu m2 = new ConsMenu(steak,m1);
IMenu m3 = new ConsMenu(veggie,m2);
. . . checkExpect(m3.select().sort(),. . . ,"composing select and sort") . . .
. . . checkExpect(m3.sort().select(),. . . ,"composing sort and select") . . .
Selecting first and then sorting should produce the same result as sorting
first and then selecting, at least as long as both test cases use the same sorting and selection criteria. Alas, only the first test type-checks; the second
one doesnt because sort produces an IList not an IMenu and the former
doesnt implement the select method.
Right here, you may realize that defining the lower tier in figure 184 corresponds to the testing step in the design recipe of section 32. This step calls
for overriding all method definitions that dont match the desired type.
More specifically, it suggests using a context-specific signature for the sort
method and defining it with the help of casts and super calls:
inside of IMenu :
IMenu sort();
inside of ConsMenu :
public IMenu sort() {
IMenu result = (IMenu)super.sort();
return result;
}
Once these changes are in place, both of the above tests type-check.
Unfortunately, evaluating the tests stops with an exception:
Exception in thread "main" java.lang.ClassCastException: Cons
at ConsMenu.sort ...
The first line of this text tells you that your program evaluation failed because a cast failed. Specifically, the cast found a Cons where it expected
something else. The second line pinpoints the cast that failed. Here it is the
cast that the sort method in ConsMenu needs in order to turn the result of
the super.sort() call into a menu:
535
inside of ConsMenu :
public IMenu sort() {
IMenu result = (IMenu)super.sort();
return result;
}
In short, result is not made up of objects that implement IMenu, but of instances of Cons and Mt.
On second thought the exception shouldnt surprise you. An inspection
of sort in Mt and Cons shows that it creates lists with Mt and Cons. Since
neither of these classes implements IMenu, the cast from the result of sort
to IMenu must fail.
At this point, you might think that the design of sort should have used
ConsMenu instead of Cons. Doing so, however, is wrong because the upper
tier in figure 183 is a library that is useful for representing many different
kinds of data, including menus, electronic phone books, and others. Separating those two tiers is the point of creating a widely useful abstraction.
The conclusion is thus obvious:
WARNING
ON
F RAMEWORK E XTENSIONS
If so, any call to the respective methods creates instances of the original
library, not the extension.
536
inside of MtMenu :
public IMenu insert(IComp o) {
return new ConsMenu(o,this);
}
Section 34
inside of ConsMenu :
public IMenu insert(IComp o) {
if (o.lessThan(first)) {
return new ConsMenu(o,this); }
else {
return
new ConsMenu(first,rest.insert(o)); }
}
}
While doing so solves the problem, it mostly duplicates a method and creates an obvious candidate for abstraction. After all, the definitions of sort
in the library and in its extensions are equal up to the constructors. Sadly,
though, it is impossible to abstract over constructors or its return types, a
problem we have encountered seen several times now.
It is possible, however, to introduce another method that uses the constructor and to use it in place of the constructor:
inside of MtMenu :
public IMenu insert(IComp o) {
return factory (o, factoryMt ());
}
inside of ConsMenu :
public IMenu insert(IComp o) {
if (o.lessThan(first)) {
return factory (o,this); }
else {
return factory (first,rest.insert(o)); }
}
}
Naturally the methods factory and factoryMt just mimic the constructors:
IList factoryMt() {
return new Mt();
}
Now there is no need to copy the definitions of sort and modify them; it
suffices to override them if you also override the two factory methods in
each class:
IMenu factoryMt() {
return new MtMenu();
}
537
interface IList {
...
// sort this list, according to lessThan
IList sort();
// insert o into this (sorted) list
IList insert(IComp o);
// factory method for Cons
// extensions must override this method
IList factory(Object f , IList r);
// factory method for Mt
// extensions must override this method
IList factoryMt();
}
class Mt implements IList {
...
public IList sort() {
return this;
}
Section 34
538
interface IMenu {
...
// sort this list, according to lessThan
IMenu sort();
// factory method for Cons
// extensions must override this method
IMenu factory(Object f , IMenu r);
// factory method for Mt
// extensions must override this method
IMenu factoryMt();
}
class MtMenu implements IMenu {
...
public IMenu sort() {
return factoryMt();
}
539
Exercises
Exercise 34.5 Develop examples and tests that demonstrate the proper behavior of both the original list library (figure 185) as well as its extension 186.
Exercise 34.6 The factory method definitions in Cons are identical to those
in Mt in figure 185. Abstract them. Start with a revised class diagram then
change the class and method definitions. Use the tests of exercise 34.5 to
ensure that the abstraction didnt introduce typos.
Question: Is it possible to use an analogous abstraction for the same
commonalities in the lower ties, i.e., in the function extension of the library
of figure 186? If possible, do so; if not explain why.
Exercise 34.7 Design the method remove for the library figure 185. Extend
the tests from exercise 34.5 to ensure that remove works properly with the
library extension.
Section 34
540
interface IMenu {
// how many items are on this menu?
int count();
// does this menu contain an item named i?
boolean contains(String o);
// render this menu as a String
String asString();
// select those items from this menu
// that cost between $10 and $20
IMenu select();
// sort this menu by price
IMenu sort();
}
541
a method for selecting items from a list, and the method therefore cant
just dispatch as sort does. One thing we could try is to design a private,
auxiliary method in Menu for filtering lists and create a menu from the
result:
inside of Menu :
public IMenu select() {
return new Menu(selectAux(items));
}
This definition assumes of course that we can solve the following programming problem:
inside of Menu :
// select those items from items that cost between $10 and $20
private IList selectAux(IList items) {
The most important point to notice is that the purpose statement cannot
(and does not) refer to this menu; instead it talks about the list parameter
of the method.
As it turns out, we must remember our earliest experiences with lists
and the generalization of design recipes to recursive data representations.
From the perspective of selectAux, a list of menu items is defined like this:
Section 34
542
543
inside of Menu :
private IList selectAux(IList items) {
Cons c;
MenuItem m;
IList r;
if (items instanceof Mt) {
return new Mt(); }
else {
c = (Cons)items;
m = (MenuItem)c.first;
if (m.hasProperty()) {
return new Cons(m,selectAux(c.rest)); }
else {
return selectAux(c.rest); }
}
}
Using the casts poses no danger here, because the if statement has established that items is an instance of Cons for the else-branch. The casts are
only used to make the type checker accept the method. Include selectAux
in the Menu class and test it.
Our solution suffers from two problems. First, the design is not based
on the class-based representation of the menu, but on a functional interpretation of the data. It is not object-oriented. Second, the design assumes that
the fields of Cons objects are accessible. If the designer of the list library had
followed the recommendations of section 20.4, however, the fields would
be private and thus inaccessible to others. In that case, we simply wouldnt
be able to design selectAux.
What this suggests and what other sections in this chapter have suggested is that a list library such as ours should provide a method for inspecting every item on the list. In How to Design Programs, we have encountered several functions: map, filter, foldl, and so on. We also worked out how
to design such functions for arbitrary data representations. Clearly, classbased libraries need equivalent methods if others are to design additional
functionality for them.
In general then, the designers of data libraries face a choice. Since they
cannot anticipate all possible methods that their future users may want,
they must either turn the library into an extensible framework (as explained
in this section) or they must prepare it for arbitrary traversals. The next
chapter shows how to implement this second choice.
Section 34
544
Exercise
Exercise 34.10 When you design classes that use a library, you should program to the librarys interface. In our running example, you should design
the selectAux method by using the IList interface. As discussed, this isnt
possible because selectAux uses instanceof and accesses fields.
Modify the list library so that you need neither instanecof nor field access to define selectAux. The former was dealt with in the preceding chapter. Section 33.2 addresses the issue of accessing fields via interfaces. Here
we propose this specific change:
inside of IList :
// is there another item on this list?
boolean hasNext();
// retrieve the first item from this list
// ASSUMES: this list has next: this.hasNext()
Object next();
// retrieve the rest of this list
// ASSUMES: this list has next: this.hasNext()
IList getRest();
Once you have modified the list library, re-formulate selectAux using these
new methods.
Would this kind of extension continue to work if you were to add a
Range-like variant (see subsection 34.1) to the list library?
the terminology of How to Design Programs, a program that solves an instance of the
Towers of Hanoi puzzle illustrates generative recursion.
545
with a hole in the middle are stacked up on the left pole. The largest disk
is at the bottom of the stack; the second largest is in the middle; and the
smallest is on top. The player must move the disks from the left pole to
the right pole, one disk at a time, using the middle pole as an intermediate
resting point. As the player moves one disk, it may not come to rest on top
of a disk that is smaller.
Your graphical program should use a simple white canvas to draw the
current state of the puzzle. See the top-most screen shot in figure 188 for a
drawing of the initial state of the puzzle. A player should specify the movement of disks by pressing single-character keys on the keyboard. Specifically, if the player presses the letters L, M, or R for the first time,
your program should prepare the move of the top-most disk from the left
pole, middle pole, or right pole, respectively. Since pressing such an action key puts your program into a different state, your canvas should reflect this change; it should indicate what it is about to do. As the second
screen shot in figure 188 shows, our solution places a string at the top-left
corner of the canvas that specifies from where the player is about to remove
the next disk. Finally, when the player presses one of those three letter keys
again, the program completes the move. See the bottom most screen shot
in figure 188 and also note that the message has disappeared. The program
is ready to perform the next disk move.
Section 34
546
The objective of this mini project is to use what you have learned in
this chapter: the design and re-use of data representation libraries; their
adaptation to new situations; and the creation of extensible frameworks
from libraries. As always, the following series of exercises begins with a
top-down plan and then proceeds with a bottom-up design of classes and
methods. The section ends with an integration of the pieces in the World
of Hanoi class.
You are going to face two design choices as you solve the following
exercises. One concerns the notion of generics; the other one the use of a
function extension in the spirit of section 34.3 or 34.4. In short, there are
four major outcomes possible based on your current design knowledge.
We encourage you to design as many of these choices as you have time for.
Exercises
Exercise 34.11 The Tower of Hanoi puzzle clearly deals with a world of
disks and poles, but also stacks and lists. See section 32.4 if you have forgotten about stacks.
Design a data representation, starting with a class that extends World
from the idraw or draw library. Focus on the essence of the objects.
You should start this step with a class and interface diagram, and you
should finish it with data examples.
Exercise 34.12 Design a complete data representation for disks. Equip the
class with a method for drawing a disk into a canvas at some given x and y
coordinates.
Exercise 34.13 Design a generally useful library for representing stateful
stacks based on applicative lists:
1. The Stack class should support methods for pushing items on top of
the stack; retrieving the top item from a stack; popping the first item
off the stack; and determining its current depth and whether the stack
is empty.
2. The IList interface, which specifies that lists support the methods of
retrieving the first item, retrieving the rest of the list, determining its
length and whether it is empty.
3. The Mt class, which implements IList and provides a constructor of
no arguments.
547
4. The Cons class, which implements IList and provides the usual twoargument constructor.
Indicate whether you choose an approach based on subtyping or generics
or both.
Exercise 34.14 Design a data representation for poles. Since a pole acts
like a stack of disks, you must re-use the stack and list library from exercise 34.13 and the disk representation from exercise 34.12. In doing so, ensure that, for example, the method for retrieving the top-most item actually
returns a disk.
Equip the data representation with a method for drawing the pole and
the disks on the pole into a canvas at some given x and y coordinates. Doing so you are facing a choice with two alternatives: you may either use
the traversal-based design from this section or the design of extending the
functionality of the list representation of the library. After all, the list representation doesnt come with a method for rendering itself on a canvas.
Exercise 34.15 Design the class Hanoi, which extends World and uses the
results of exercises 34.11 to 34.14 to implement a game for solving threedisk Tower of Hanoi puzzles.
When you have solved the problem and the puzzle to your satisfaction,
consider what it takes to generalize the program so that players can specify
the number of disks in the initial configuration (with or without limits).
548
Intermezzo 5
Generic Classes
549
550
TODO
Intermezzo 5
VI
Many methods have the purpose of traversing compound data and collecting data from such traversals (all in the form of objects). Some methods search lists or trees or graphs for objects with certain properties; other
methods collect information from all of them and combine it; and yet others
modify all items in a uniform manner.
If you look closely, these methods share a lot of basic elements with
each other. Bluntly put, they all follow a similar pattern and writing this
same pattern over and over again must violate your sense of proper abstraction. At the same time, these patterns dont fall into the abstraction
categories of the preceding chapters, so even if you recognized the problem, you couldnt have abstracted yet.
The lack of abstract traversals also poses a problem for the design and
use of so-called collection libraries. When programmers design libraries
for collections, they must anticipate the methods that are most useful. Of
course, it is generally impossible to anticipate them all. Hence, the programmers who use the library may have to create extensions to supply the
missing functionality. As the preceding chapter has shown, such extensions
are difficult to build in general and, once built, are difficult to maintain. If
a library offers a general traversal method, however, designing additional
functionality is usually straightforward.
Here we discuss how to design a general traversal method for a data
structure library and how to design uses of such traversal methods. We also
discuss an imperative traversal method andin preparation of the next
chapterhow it is often abused for applicative computations.
36 Patterns in Traversals
The preceding chapter deals with restaurant menus and their data representations. The emphasis there is on creating representations that re-use
Section 36
552
interface IMenu {
// select the items from this menu that cost between $10 and $20
IMenu selectByPrice();
// select the items from this menu that do not start with "Meat:"
IMenu selectByMeat();
}
class Mt implements IMenu {
// constructor omitted
public IMenu selectByPrice() {
return this;
}
class MenuItem {
private String name;
private int val; // in dollars
public MenuItem(String name, int val) {
this.name = name;
this.val = val;
}
Patterns in Traversals
553
9
13
18
21
16
As you can see, this restaurant takes care of its vegetarian clientele with a Meat: warning at the head of the line.
Design a menu representation that allows customer to select
those items on the menu that cost between $10 and $20 (inclusive) and those that are vegetarian. . . .
Section 36
554
Cons: The Cons class extends an existing menu with another instance of
MenuItem. Pay close attention to the two selection methods; they
are so similar that they are nearly indistinguishable. The first, selectByPrice, uses the hasGoodPrice method from MenuItem. In its place,
the second one, selectByMeat, uses the hasNoMeat method.
Of course, in the real world we would use a field to designate a menu item
as a non-meat item; here we just use the structure of the MenuItem that you
know from the preceding section.
The last clause in the enumeration points out the problem in detail.
Here it is again with the differences high-lighted:
public IMenu selectByPrice() {
IMenu r = rest.selectByPrice();
if (first. hasGoodPrice ()) {
return new Cons(first,r); }
else {
return r; }
}
The two method definitions from Cons are alike except for the two method
calls in gray. If you had encountered such a problem in the context of How
to Design Programs, you would have abstracted over the different methods
with an additional parameter, and the two method definitions would have
been identical after that.
In the world of Java, abstracting over methods is impossible. Unlike
functions in Scheme, methods are not first-class values. Still, the idea of
abstracting over the method call as if methods were first-class objects is
what it takes to avoid this kind of code duplication. Before we do that,
however, lets look at the menu problem from a slightly different angle.
Exercise
Exercise 36.1 Design a data representation for menu items that allows a
proper accommodation of dietary restrictions (e.g., kosher, vegan, etc). At
a minimum, make sure a dish is properly designated as vegetarian or not.
Patterns in Traversals
555
interface IPred {
// does this item have a property?
boolean good();
}
interface IList<I extends IPred> {
// select the items from this list
// that satisfy good (in IPred)
IList<I> select();
}
class Mt<ITEM extends IPred>
implements IList<ITEM> {
// constructor omitted
}
class MenuItem implements IPred {
private String name;
private int val; // in dollars
556
Section 36
tion from scratch. Since menu items come and go, it is obvious to identify
menus with listsat least those we have dealt with here.
Figure 190 spells out the details of this approach. Its top-half shows
a generics-based list library that supports a single method: select, because
this is all we need. The method traverses the lists and produces a list whose
items produce true when their good method is invoked. The latter is specified in the IPred interface of the library.
Using this library requires the design of a class C that implements IPred.
Instances of C can then be collected in an instance of IList<C>. For our specific case, you need to design the MenuItem class so that it has a public good
method and then you can use IList<MenuItem> as a data representation
for menus.
The bottom-half of figure 190 displays not one, but two designs of MenuItem, and as you can tell, they are distinct. The one on the left supports a
good method that returns true if the item costs between $10 and $20; it is useful for cost-conscientious customers. In contrast, the class on the right defines a good method that identifies vegetarian menu items, i.e., those suited
for a vegetarian visitor.
Unfortunately, it is impossible to combine the two classes in the given
context. The list library has settled on the name good as the one criteria to
be used when objects are selected from a list. If there is a need to use two
distinct criteria to select sub-lists from one and the same list, you cannot use
this library to achieve this goal.
You may recall that we have encountered an instance of this very same
problem in the preceding chapters in the shape of the sort problem. Just like
select in the library of figure 190 use a fixed good method to pick the appropriate items, the sort method uses a fixed lessThan method to arrange the
items in the desired order. Sorting the same list according to two different
criteria is impossible because it is impossible to abstract over methods.
Patterns in Traversals
557
(define (select1 l)
(define (select2 l)
(cond
(cond
[(empty? l) empty]
[(empty? l) empty]
[else
[else
(if ( hasGoodPrice (first l))
(if ( hasNoMeat (first l))
(cons (first l) (select1 (rest l)))
(cons (first l) (select2 (rest l)))
(select1 (rest l)))]))
(select2 (rest l)))]))
we would have abstracted over them like this:
(define (select-abstract l predicate )
(cond
[(empty? l) empty]
[else
(if ( predicate (first l))
(cons (first l) (select-abstract (rest l) predicate ))
(select-abstract (rest l) predicate ))]))
That is, we would have introduced an additional parameter, passed it along
to all recursive function calls, and used it where we used to call specific
functions. In order to validate this abstraction, we would eventually have
defined the original functions in terms of select-abstract:
(define (select1 l)
(select-abstract l hasGoodPrice ))
(define (select2 l)
(select-abstract l hasNoMeat ))
Section 36
558
inside of Cons :
// select a sublist of items for which predicate holds
public IMenu selectBy(IMethod predicate) { . . . }
Like above, predicate is the name of a method parameter, i.e., it stands for
an object that acts like a method. The name of its type, IMethod suggests an
interface that describes how to represent methods as objects. In this specific case, the predicate object represents methods that determine whether
an instance of MenuItem has a certain property.
Our next step must be to translate this requirement on predicate into a
method signature for the IMethod interface. The word determine implies
that if IMethod describes a method, it is a boolean-valued method. In turn,
the phrase some instances has a property means the method consumes
such an instance:
interface IMethod {
// determine whether m has this property
boolean good(MenuItem m);
}
Put differently, IMethod represents objects that support one method. This
method consumes a MenuItem and produces a boolean. If you ponder this
for a moment, it shouldnt surprise you that such objects represent methods. After all, all you can do with them is invoke exactly one method.
Equipped with IMethod and an understanding how predicate represents
a method, the next step follows logically, without a choice:
inside of Cons :
public IMenu selectBy(IMethod predicate) {
IMenu r = rest.selectBy(has);
if ( predicate.good(first) )
...
}
The test expression in gray invokes the method in predicate on the first MenuItem. The if-test then uses the result to continue as before.
If we can actually implement the IMethod class, the new selectBy method
works and works for general selection criteria. Since abstraction demands
that we show how to get the old programs back, lets see how we could
represent the hasGoodPrice method. To do so, we act as if we are designing
a method using the design recipe for methods but we do design an implementation of IMethod with a specific purpose:
Patterns in Traversals
559
interface ISelect {
// determine whether m has this property
boolean good(MenuItem m);
}
interface IMenu {
// select the items from this menu according to has
IMenu selectByMeat(ISelect);
}
class Mt implements IMenu {
// constructor omitted
}
class HasGoodPrice
implements ISelect {
// is ms cost betw. $10 and $20
public boolean good(MenuItem m) {
return m.hasGoodPrice();
}
}
class HasNoMeat
implements ISelect {
// is m item free of Meat:
public boolean good(MenuItem m) {
return m.hasNoMeat();
}
}
Section 36
560
Exercise
Exercise 36.2 For some diners, a price range of $10 to $20 could be too expensive, for others, it may sound cheap. Design a variant of HasGoodPrice
that allows that customers to select a price range for the selection process.
Exercise 36.3 Re-equip the menu representation in figure 191 with the selectByPrice and the selectByMeat methods of figure 189. Doing so ensures
that the former is as powerful and as useful as the latter.
Exercise 36.4 Use the methods-as-objects idea to abstract the select method
in the list library of figure 190 (see section 36.2). We suggest you proceed as
follows:
1. Design the class Menu, which represents a menu as information about
todays date, the restaurants name, and todays menu. Use lists for
Patterns in Traversals
561
the latter. Equip the class with methods for selecting a well priced
menu from the existing menu.
2. Design the abstraction of the select method.
3. Re-design the selection method from item 1 so that it uses the abstraction from item 2. Also design a method for selecting a vegetarian
sub-menu.
Section 36
562
interface IFun<I> {
// create new data from theItem
I process(I theItem);
}
interface IList<I> {
// apply f to each item on this list; collect in new list
IList<I> map(IFun<I> f );
}
class Mt<I>
implements IList<I> {
// constructor omitted
class Cons<I>
implements IList<I> {
private I first;
private IList<I> rest;
// constructor omitted
}
class Add1
implements IFun<Integer> {
public Integer process(Integer i) {
return i + 1;
}
}
class Sub1
implements IFun<Integer> {
public Integer process(Integer i) {
return i 1;
}
}
The IFun interface is similar to the one for the select method in the preceding subsection:
inside of IFun<I> :
I process(I theItem);
We know from the signature for map that its processing function consumes one list item at a time and produces another one. This knowledge
Patterns in Traversals
563
Section 36
564
inside of Examples :
checkExpect(l3.map(new Add1()),r4,"add 1 to all")
checkExpect(r4.map(new Sub1()),l3,"sub 1 to all")
They express in our testing framework what the preceding paragraphs say.
informally.
Putting together the template for map follows proven procedures:
inside of Mt<ITEM> :
I map(IFun<ITEM> f ) {
...
}
inside of Cons<ITEM> :
I map(IFun<ITEM> f ) {
. . . first . . . rest.map(f ) . . .
}
Once you have the template, defining the map method itself is as easy as
designing a method in chapter II. The result of the coding step is on display
in figure 192; take a good look, study it in depth, and solve the following
exercises before you move on.
Exercises
Exercise 36.5 Design a data representation for the inventory of a grocery
store, including at least a name (String), a producer (String), a price (a double), and an available quantity (int). Then equip the representation with two
applicative methods:
1. inflate, which multiplies the price of each item with 1.1;
2. remove, which sets the available quantity of all items from a specific
producer to 0.
Finally abstract over the two methods by designing a map-like method.
Exercise 36.6 Abstract the classes Add1 and Sub1 in figure 192 into a single
class that implements IFun<Integer>.
Exercise 36.7 Design the following implementations of IFun<ITEM> (for
an appropriate type ITEM):
1. Not, which when applied to an instance of Boolean negates it;
2. Hello, which when applied to an instance of String (referring to a
name) prefixes it with "hello ";
3. Root, which when applied to a Double computes its square root.
Patterns in Traversals
565
Develop a test suite that uses these classes to map over lists of Booleans,
Strings, and Doubles, respectively.
Exercise 36.8 Design the following implementations of IFun<ITEM> (for
an appropriate type ITEM):
1. And, which when applied to an instance of Boolean computes the conjunction with some other, fixed but specifiable Boolean;
2. Append, which when applied to an instance of String prefixes it with
some other, fixed but specifiable String;
3. Multiply, which when applied to an instance of Double multiplies it
with some other, fixed but specifiable Double.
Develop a test suite that uses these classes to map over lists of Booleans,
Strings, and Doubles, respectively.
The exercises demonstrate how useful the map method in figure 192 is.
At the same time, they drive home the point that map, as currently available, always consumes a list of some type and produces one of the same
type. Clearly, this form of traversal function is overly restrictive. As you
may recall from How to Design Programs or from this book, list traversals of
the kind that map represents are common, but usually they produce a list
of a different type than the one given.
Consider the simplistic problem of determining for each integer on a
list of integers whether it is positive (or not). That is, given a list of integers
loi, you want an equally long list of booleans lob such that if the ith item on
loi is positive then the ith item on lob is true. Processing a list of integers in
this manner is clearly a map-style task but defining an appropriate method
as an object fails:
class IsPositive implements IFun<Integer> { // ILL-TYPED CODE
public Boolean process(Integer i) {
return i > 0;
}
}
As you can easily see, the signature of the process method in this class does
not match the signature in IFun<Integer>, and therefore the type checker
cannot approve the implements clause.
Section 36
566
A close look at this example suggests a first and obvious change to the
function interface of the list library:
interface IFun<I,RESULT> {
// create new data from theItem
RESULT process(I theItem);
}
While the definition of figure 192 parameterizes functions only over the
type of the list items, this revised definition parameterizes functions over
both the type of the items as well as the type of the result. Hence, in this
context, you can define the IsPositive class just fine: see the bottom right
class in figure 193. Note, though, that it implements an IFun interface at
Integer, the type of the list items, and Boolean, the type of the result.
So now you just need to check whether this definition of IFun works
with the rest of the list library. Lets start with IList:
inside of IList<ITEM> :
IList<R> map(IFun<ITEM,R> f );
Since the interface for functions requires two types, we provide I and R.
The former, I, represents the type of list items; the latter, R, is the result
type of the function. In other words, IFun<I,R> is the transliteration of (X
Y) into Javas type notation. From here, you can also conclude that the
result type of map must be IList<R>, because f creates one instance of R
from each instance of I and map collects those in a list.
Problem is that R comes out of nowhere, which you must find disturbing. If you were to submit this revised interface definition to Javas type
checker as is, it would report two errors:
... cannot find symbol ... R
one per occurrence of R in the code. These error messages mean that R is
an unbound identifier; since we want it to stand for some concrete type, it
is an unbound or undeclared type parameter.
Thus far, we know only one place where type parameters are declared:
to the immediate right of the name of a class or interface definition. You
might therefore wonder whether R should be a parameter of IList:
interface IList<I,R> {
IList<R> map(IFun<I,R> f )
}
Patterns in Traversals
567
interface IFun<I,R> {
// create new data from theItem
R process(I theItem);
}
interface IList<I> {
// apply each item to process in f , collect in new list
<R> IList<R> map(IFun<I,R> f );
}
class Mt<I>
implements IList<I> {
// constructor omitted
public <R>
IList<R> map(IFun<I,R> f ) {
return new Mt<R>();
}
class Cons<I>
implements IList<I> {
private I first;
private IList<I> rest;
// constructor omitted
}
class Add1
implements
IFun<Integer,Integer> {
public Integer process(Integer i) {
return i + 1;
}
}
public <R>
IList<R> map(IFun<I,R> f ) {
R fst = f.process(first);
return new Cons<I>(fst,rest.map(f ));
}
class IsPositive
implements
IFun<Integer,Boolean> {
public Boolean process(Integer i) {
return i > 0;
}
}
Obviously this interface definition type checks. Declaring the type parameter in this manner poses a serious obstacle, however, as a quick thought
experiment demonstrates. Specifically, consider a Menu class that uses
IList<I,R> to encapsulate a field of menu items:
Section 36
568
class Menu {
IList<MenuItem,XYZ> items;
...
}
The XYZ indicates the missing return type for map in IList. Putting a type
here fixes the signature of map to
IList<XYZ> map(IFun<MenuItem,XYZ> f )
once and for all. In other words, all uses of map within Menu must produce
lists of XYZs. This is, of course, impractical because the Menu class may
have to use map at many different return types. Before you read on, come
up with three examples of how map could be used with distinct return types
for menus.
Our conclusion must be that the result type R for map functions cannot
be bound for the entire interface. Instead, it must be instantiated differently
for every use of map. In Java, we can express this independence of maps
result type from ILists type parameters with the introduction of a mapspecific type parameter:
inside of IList<ITEM> :
<R> IList<R> map(IFun<ITEM,R> f );
Syntactically, you do so by pre-fixing a method signature with a list of type
parameters in angle brackets. Here a single type, R, suffices. Invocations of
map implicitly specify R from the type of the IFun argument, and the Java
type checker uses this type for the result list, too. Method signatures such
as maps are said to introduce POLYMORPHIC METHODS.
Figure 193 shows the class and interface definitions for a completely
functional list library with a versatile abstraction of a map-style traversal.
Other than the generalizations of maps signature and the IFun interface,
nothing has changed from figure 192. The bottom part displays two classes
that implement the new IFun definition; take a good look and get some
practice by solving the following exercises.
Exercises
Exercise 36.9 Recall the parametric Pair class from chapter V (page 461).
In this context, design the following implementations of IFun<I,R> (for
appropriate types I and R):
1. First, which when applied to an instance of Pair<Integer,Boolean> returns the left part of the object;
Patterns in Traversals
569
interface IList<I> {
interface IMap<KEY,VALUE> {
// f.process() each item, collect in new list
// add the entry [k,value] to this
<R> IList<R> map(IFun<I,R> f );
void add(KEY k, VALUE value);
// select the items from this are f.good()
IList<I> select(IPredicate<I> f );
// is x a member of this?
boolean member(I x);
Exercise 36.10 An ASSOCIATION MAP is like a dictionary. Instead of associating words with explanations of their meanings, though, it associates
arbitrary keys with values. After creating a map, you add key-value pairs;
you may then check whether a given map associates a key with any value;
and you may retrieved the value that a map associates with a key.
The right side of figure 194 displays an interface that has turned this
informal description into a rigorous specification. design an association
map library that implements this IMap interface. Start with composing a
list library with the interface specified on the left in figure 194; use whatever
pieces are available, but do develop a test suite.
Section 36
570
interface IListInt {
// the sum of integers on this list
int sum();
// the product of integers on this list
int pro();
}
class MtInt implements IListInt {
public MtInt() { }
boolean, string, etc. This is what How to Design Programs and functional
programming call a fold operation, but it has also found its way into objectoriented programming languages from there.
Figure 195 shows two fold-style list traversals in a data representation
of lists of integers. The first one is the sum method, which adds up all
the numbers on the list. The second is the pro method, which computes
the product of all the numbers. Both methods traverse the entire list and
compute a single number by combining the list items with + or . When
sum reaches the end of the list, it produces 0; pro, in contrast, returns 1.
Following the design recipe for abstraction, figure 195 highlights the two
pairs of differences in gray.
Figure 196 displays a list-of-ints representation that abstracts over these
two traversals, including the introduction of an abstract class to abstract
Patterns in Traversals
571
abstract
class AListInt implements IListInt {
// process each item of this list,
// collect results with f ; start with e
abstract int arith(IOp f , int e);
interface IOp {
// a function that combines x and y
int combine(int x, int y);
}
}
class Sum implements IOp {
public int combine(int x, int y) {
return x + y;
}
}
over the commonalities once the sum and pro methods are abstracted. Lets
take one step at a time, starting from the differences.
Since the two traversals differ in two places, you should expect that
the abstracted traversal functions consumes two arguments. Figure 196
confirms this with the gray line in the AListInt class at the top:
Section 36
572
inside of AListInt :
abstract int arith(IOp f , int e);
The first parameter abstracts over the operation that the traversal uses to
collect the results. The second parameter abstracts over the constant that
the traversal uses when it encounters an empty list. The second one is also
easy to explain. Both constants are ints in the original traversal, meaning
that the new arith method just consumes the appropriate int constant.
The first parameter of the new arith method requires an explanation.
Unlike in Scheme, where operations are functions, Java does not recognize
operations as first-class values. Hence, it is once again necessary to encode
something as objects; IOp is the interface that we use to specify the behavior
of these objects.
Based on the signature of arith and its explanation, the design of the
arith methods is straightforward. For MtInt, the method just returns e; for
ConsInt, the method uses the function argument f to combine the first
item on the list and the result of processing the rest of the list.
Once you have an abstraction of two traversals, you need to recover the
original operations. The first step is to create method invocations of arith
that simulate the traversals:
// aList.sum() is equivalent to:
aList.arith(new Sum(),0)
You can see the definitions of Sum and Pro at the bottom of figure 196. They
implement IOp in the expected manner. Of course, the ideal abstraction
process ends up restoring the two traversals and offering designers the option of defining more such methods. A moments thought shows that doing
so would leave you with two pairs of identical copies of sum and pro, respectively. Because you wish to avoid such replications, you would follow
the design recipe of chapter III, which yields the abstract class in figure 196.
Exercises
Exercise 36.11 Develop a test suite for the list library of figure 195 and use
it to test the library of figure 196, too.
Exercise 36.12 Add the methods isEmpty and first to figure 196. The latter
should assume that the list is not empty.
Use these methods and arith to design the method max (min), which
finds the largest (smallest) item on a non-empty list of ints respectively.
Patterns in Traversals
573
interface IFun<X,Z> {
// create new data from i and e
Z process(X i, Z e);
}
interface IList<I> {
// f processes each list item and collects results
// start with e if there are no items
<Z> Z fold(IFun<I,Z> f , Z e);
}
class Mt<I> implements IList<I> {
public Mt() { }
public
<Z> Z fold(IFun<I,Z> f , Z e) {
return e;
}
public
<Z> Z fold(IFun<I,Z> f , Z e) {
return f.process(first,rest.fold(f ,e));
}
Exercise 36.13 In the context of figure 196, a list is a union of two variants.
Add a Range variant for representing intervals of integers. See section 33.3
for the idea behind Range. Ensure that sum and pro just work.
You may realize from reading How to Design Programs that arith is a
fold method, i.e., a method that folds together an entire list into a single
value. Generalizing arith to fold is simple. First, you generalize the type of
the list items from int to I, a type parameter:
inside of AListInt<I> :
int arith(IOp2<I> f , int e)
inside of IOp2<I> :
int combine(I item, int y)
Section 36
574
That is, both arith and combine (from IOp) process Is not ints as inputs. The
remaining occurrences of int in the code refer to the results of arith and combine, respectively.
Naturally, a generalization of arith shouldnt stop with a parameterization of the input type. It should also use a type parameter, say Z, to abstract
over the result:
inside of IListInt<I> :
<Z> Z fold(IFun<I,Z> f , Z e)
inside of IFun<I,Z> :
int combine(I item, Z y)
Of course, the type parameter isnt a parameter of the list interface, but only
of the fold method itself. Put differently, just like map from the preceding
section, the fold method is parametric. The specification of the combine must
change in analogous manner.
This is all there is to the abstract fold method. For the full definition, see
figure 197. As you can see from the figure, the abstraction of arith to fold
is really just about type abstractions, not an abstraction of the mechanism
per se. The method definitions remain the same; that is, the body of fold in
Cons<I> is identical to the body of fold in ConsInt and the body of fold in
Mt<I> is the same as the body of fold in MtInt.
Exercises
Exercise 36.14 The abstraction of arith to fold isnt complete without showing that you can redefine some of the motivating examples. Demonstrate
that fold can be used to define sum and pro for lists of integers.
Exercise 36.15 Design a data representation for teams. A single team has
two attributes: a name and a list of members, represented via first names
(String). Add a method that computes a collective greeting of the shape
"Dear Andy, Britney, Carl, Dolly, Emil:"
assuming the team members have the names "Andy", "Britney", "Carl",
"Dolly", and "Emil".
Exercise 36.16 Design a class that represents a list of measurements as a
String (for the source of the measurements) and a list of Doubles, created
with the list library of figure 197. Also design a method that determine
whether all measurements are positive.
Patterns in Traversals
575
Exercise 36.17 Design a class that represents a bill as a String (for the customer) and a list of pairs of type Pair<String,Integer> for the items sold and
their cost. (See chapter V (page 461) for Pair.) Also design a method that
computes the sum total of the billed items.
Section 36
576
elements that are essential for the original problem statement: numbers,
variables, and the combination of two expressions into one.
The second step is to choose interfaces and classes to represent this kind
of information as data and to ensure that you can translate all the informal
examples into the chosen data representation.94 Figure 198 shows our representation. Not surprisingly, it consists of an interface and three implementing variants. The interface represents the type as a whole and descriptions of the methods that it supports. The classes represent the three data
variants.
Exercise
Exercise 36.18 Translate the informal examples of variable expressions into
data examples. How would you represent 10 + x + z? Which of the two
representations do you like best?
Interpret the following data in this context: new Pls(new Var("x",new
Val(2))).
Designing the required methods is straightforward from here. Lets use
the examples from above to agree on behavioral examples:
Example
55
5+x
z
3+2
1+1+3
Variable Free?
yes
no
no
yes
yes
Value
55
n/a
n/a
5
5
The table illustrates the meaning of variable free and value of expression. It also makes it obvious why looking for the value of expressions
with variables makes little sense; until you know the value of the variables,
you cant determine the value of the expression. The only line that may
surprise you is the third one, which deals with an expression that consists
of just one item: a variable (z). Although this example is an unusual one,
our methods must deal with it, and you should keep in mind that dealing
with such examples at the level of information is the best we can do.
94 You might wonder how a method could turn a textual representation of expressions,
say "x + z + 10", into the chosen data representation. This problem is not the one we
are dealing with. A typical introductory book might give an ad hoc answer. The proper
approach is to study the concept of parsing and its well-developed technology.
Patterns in Traversals
577
interface IExpression {
// is this expression free of variables?
boolean variableFree();
// compute the value of this expression; ASSUMES: variableFree()
int valueOf ();
}
class Num implements IExpression {
private int value;
public Num(int value) {
this.value = value;
}
Section 36
578
Figure 198 displays the complete solution of the problem, for the restricted set of expressions. The interface contains the two obvious signatures and purpose statements. The methods in the implementing classes
are straightforward, except that the ASSUMES part is expressed as a single test in the one relevant class instead of a test in each.
Exercises
Exercise 36.19 Use the examples from the above table to develop a test
suite for the code in figure 198.
What do you think the results for 10 + z + 55 and 10 + 20 + 30 should
be? Which of the possible data representation do you like best? Does it
make a difference which one you choose? Why or why not?
Exercise 36.20 Design an extension of the data representation of figure 198
that deals with multiplication expressions.
Exercise 36.21 Compare the two methods, variableFree and valueOf , on a
class-by-class basis. Highlight the differences. Can you abstract over the
two traversals?
The last exercise exposes the similarities between variableFree and valueOf . If you invoke either one of them, it traverses the tree of objects that
represent the expression and processes one piece at a time. Since the analogous methods are located inside the same class, it is easy to compare them:
1. in Num, one method always returns true while the other ones result
depends on the value field:
public boolean variableFree() {
return true;
}
2. in the Var class, the variableFree method naturally produces false and
the valueOf method throws an exception:
public boolean variableFree() { public int valueOf () {
throw new RuntimeException(. . . );
return false;
}
}
Patterns in Traversals
579
3. and in Pls both methods traverse the two sub-expressions and then
combine the result:
public boolean variableFree() {
return left.variableFree()
&&
right.variableFree();
}
To do so, one uses logical conjunction and the other one uses addition.
Note: The variableFree method does not always traverse the entire
sub-expression. Explain why.
Abstracting over these differences is a tall order. Unlike in the cases of
map and fold, the differences between the methods come in many varieties.
The difference between variableFree and valueOf in Pls looks like those we
dealt with for map and fold. The difference between variableFree and valueOf in Num, however, looks quite different. While one method produces
a constant, the other one actually refers to a local field.
Given these varieties of differences, lets step back and reflect on the
nature of traversals for a moment. From How to Design Programs, we know
that to traverse a data representation means to visit each object and to process it with some function. The map method illustrates this statement in
a particularly elegant manner. It visits each item on the list and applies a
function, that is, a method as an object, to the item; eventually it collects
all the results in a list. For fold, the abstraction includes a second argument,
a constant for the empty list, and the collection process is specified by the
caller of fold.
What we are seeing here is that IExpression is implemented by three
different kinds of classes. Hence, an expression consists of many nested
instances of Pls as well as Num and Var. Since all three kinds of objects are
processed in a different manner, we should consider the idea of using three
function parameters to abstract over a traversal:
inside of IExpression :
// process all instances of
// Num in this IExpression with proNum
// Var in this IExpression with proVar
// Pls in this IExpression with proPls
??? traverse(INum proNum, IVar proVar, IPls proPls)
Section 36
580
interface IVar<R> {
R process(Var n)
}
interface IPls<R> {
R process(Pls n)
}
interface IProc<R> {
R processNum(Num n)
R processVar(Var v)
R processPls(Pls p)
}
The three methods in IProc exist because traverse visits three different kinds
of objects. In the course of doing so, it uses process.processNum to process
instances of Num, process.processVar to process instances of Var, and process.processPls to process instances of Pls.
This second way of abstracting over the three differences has an advantage over the first one, and it is arguably more object-oriented than the
first one. First, the advantage is that the three methods are bundled into
one object. It thus becomes impossible to accidentally invoke traverse on a
proNum object that doesnt match the intentions of prcessVar. Second, when
things belong together and should always be together, How to Design Programs and this book have always argued that they should reside in a single
object (structure). In this example, we are dealing with three functions
to which traverse should always be applied together. It is thus best to think
of them as one object with three methods. Or, more generally, think of an
object as a multi-entry function.95
95 While object-oriented languages could introduce objects as generalizations of first-class
closures in the spirit of functional programming languages, they instead leave it to the programmer to encode the lexical context in an object manually.
Patterns in Traversals
581
interface IExpression {
<R> R traverse(IProc<R> f );
}
interface IProc<R> {
R processNum(Num n);
R processVar(Var x);
R processPls(Pls x);
}
Section 36
582
Now that we have proper signatures and purposes statements, it is possible to design the methods in a nearly straightforward fashion. We start
with traverse and then deal with process methods. Lets look at the template
for traverse in Num:
inside of Num :
<R> R traverse(IProc<R> p) {
. . . this.value . . .
}
As you can see, the template reminds you that traverse can use this, the
value field in this, and p to compute its result (plus auxiliary methods).
Since the description of p says that its processNum method consumes an
instance of Num (and processes it), you should consider the following definition obvious:
inside of Num :
<R> R traverse(IProc<R> p) {
return p.processNum(this);
}
The design of the methods in Var and Pls proceeds in a similar manner and
yields similar method definitions:
inside of Var :
<R> R traverse(IProc<R> p) {
return p.processVar(this);
}
inside of Pls :
<R> R traverse(IProc<R> p) {
return p.processPls(this);
}
Indeed, with method overloading, you could make the three methods identical. We avoid overloading here so that the design remains useful in languages that dont support it.
Exercise
Exercise 36.22 If the IProc interface in figure 199 used overloading, the
three traverse method definitions in figure 199 would be identical. The design recipe from chapter III would then call for abstracting them via the
creation of a common superclass. Why would type checking fail for this
abstraction? You may wish to re-read intermezzo 22.3 concerning overloading and its resolution.
Patterns in Traversals
583
With the design of traverse completed, you should turn your attention to
the final step of the abstraction recipe: the re-definition of the existing methods. Here we design only the implementations of IProc<R> that compute
whether an expression is variable free and, if so, what its value is.
The return type of variableFree determines the type that we use to instantiate IProc<R>. Here it is Boolean because the original method produced
a boolean result. Designing a classcall it VariableFreethat implements
IProc<Boolean> requires the design of three methods:
1. The processNum method consumes an instance of Num and determines whether it is free of variables:
inside of VariableFree :
public Boolean processNum(Num n) {
return true;
}
The method return true without looking at n because an expression
that represents a number doesnt contain any variables. Of course,
you could have just looked at the variableFree method in Num in figure 198 and used its method body.
2. The processVar method consumes an instance of Var and determines
whether it is free of variables:
inside of VariableFree :
public Boolean processVar(Var v) {
return false;
}
The method return false without looking at v because an expression
that represents a variable is guaranteed not to be variable-free.
3. Last but not least, the processPls method consumes an instance of Pls
and must determine whether it contains a variable. Since an instance
of Pls contains two expressions in two fields, the answer is clearly
non-obvious. So we turn to our design recipe and write down the
template:
inside of VariableFree :
public Boolean processPls(Pls e) {
. . . this . . . e.left . . . e.right . . .
}
Section 36
584
Patterns in Traversals
class VariableFree
implements IProc<Boolean> {
public Boolean processNum(Num n) {
return true;
}
585
class ValueOf
implements IProc<Integer> {
public Integer processNum(Num n) {
return n.valueOf ();
}
You can also see in the figure that ValueOf implements IProc<Integer>,
meaning all three methods produce integers as a results.
One attention-drawing aspect of the two classes is the lack of any use
of v in the processVar methods. Neither VariableFree nor ValueOf needs to
know the name of the variable in v. This suggests that, at least in principle,
the signature could be weakened to
inside of IProc<R> :
R processVar()
The exercises below demonstrate that while this change in signature is feasible for the two problems that motivate the abstraction of the traversal,
other traversals exploit the full functionality of this interface. In addition,
the exercises also explore a more conservative approach to the abstraction
of variableFree and valueOf .
Exercises
Exercise 36.23 Adapt the test suite from exercise 36.19 to the data representation in figures 199 and 200. Ensure that the revised code passes the same
tests.
Section 36
586
587
Exercise 36.27 Design a traversal for IExpression from figure 199 that substitutes all occurrences of variables with numbers. To this end, the traversal
needs an association between variable names (String) and values (Integer).
Hint: Use the IMap data representation from exercise 36.10 to represent
such variable-value associations.
Section 37
588
and you have determined that you need to abstract over which methods
you want to call on anObject. Following our standard practice of using
interfaces to specify requirements, here is a simple one and a sketch of its
implementation:
// methods as objects
interface IFunction {
Object invoke();
}
This interface demands that a function object have one method: invoke. In
its most general form, this method is supposed to compute the same results
as the desired method, e.g., aMethod.
Because the behavior of methods, such as aMethod, depends on the object on which they are invokedanObject in the exampleyou can see from
this sketchy implementation of IFunction that something is wrong. Because
Function doesnt have fields and invoke takes no additional arguments, invoke has no access to anObject. Hence, it must always produce the same
result or a randomly chosen result, neither of which is useful.
The obvious solution is to pass anObject as the first explicit argument to
invoke. Put differently, an interface for function objects must demand that
invoke consume at least one argument:
// methods as objects
interface IFunction {
Object invoke(Object this0);
}
Here this0 is the object on which the abstracted method used to be invoked
and which was then passed implicitly. If it were possible to use this as a
parameter name, it would be the most appropriate one; here we use this0
on occasion to indicate this relationship.
The interface uses Object for the argument and result types, thus making the signature as general as possible. Of course, an implementing class
would have to use casts to use this0 at the appropriate type. Furthermore,
it would have to use a more specific return type than Object or the site of
the original method call would have to be modified to use a cast so that the
result is typed appropriately for the context.
For example, if you wished to use IFunction to abstract in conjunction
with the map method from figure 193, you would need casts like the following in the implementing classes:
589
// add 1 to an integer
// is the integer positive?
class Add1 implements IFunction { class Positive implements IFunction {
public Integer invoke(Object this0) { public Boolean invoke(Object this0) {
Integer first = (Integer)this0;
Integer first = (Integer)this0;
return . . . // an integer,
return . . . // a boolean,
// as in figure 193;
// as in figure 193;
}
}
While the method representations use proper return types (Integer, Boolean),
the type checker still calculates that map produces a list of Objects because it
uses IFunction as the type for the method representations. Hence, the context that uses maps result must use casts to convert them to lists of Integers
or lists of Booleans.
This situation clearly isnt satisfying. In some special cases, it is possible
to use specialized interfaces that describe the desired methods with just the
right types:
interface IFunInt2Int {
Integer invoke(Integer this0);
}
interface IFunInt2Bool {
Positive invoke(Integer this0);
}
Section 37
590
Exercise
Exercise 37.1 Design the classes Add3 and Add3Again, which implement
IFunctionN and IFun3, respectively. They both are to represent methods as
objects that consume exactly three integers and produce their sum.
Which interface would you use to produce the list of sums of a list of
lists of integers? How would you use it?
591
interface IFun0<RANGE,DOMAIN> {
RANGE invoke(DOMAIN this0);
}
interface IFun1<RANGE,DOMAIN,DOMAIN1> {
RANGE invoke(DOMAIN this0, DOMAIN1 arg1);
}
interface IFun2<RANGE,DOMAIN,DOMAIN1,DOMAIN2> {
RANGE invoke(DOMAIN this0, DOMAIN1 arg1, DOMAIN2 arg2);
}
of the arguments and the result type are parameters of the interface specification. Furthermore, if these interfaces are used to abstract over method
calls, we assume that the formerly implicit argument, that is, the object on
which the method is invoked, is passed as the first argument.
As you can guess from the arrangement in figure 201, the three interfaces represent just the beginning of a (n infinitely) long series. Since you
cant predict the kind of functions you will need to represent, you need
at least in principlesignatures for all possible numbers of parameters. In
practice, though, programmers do notand should notdesign methods
with more than a handful of parameters. Thus, spelling out, say, 10 of these
interfaces is enough.96
Exercise
Exercise 37.2 Define a generic interface that is analogous to IFunctionN at
the end of section 37.1.
592
Section 37
593
class C {
Type method2(. . . ) {
return method( new FunB(. . . ) ,. . . );
}
2. Class FunA introduces a data representation for methods that compute like methodA.
3. Class FunB is a representation for methods that compute like methodB.
Obviously the notational cost of creating extra classes and instantiating
them elsewhere is large. First, introducing three classes where there used
to be one means you and future readers need to re-establish the connection. Second, it is far less convenient than creating first-class functions with
lambda in Scheme (see How to Design Programs); so you know that there
must be a simpler way than that.
Before we discuss these additional linguistic mechanisms, let us introduce the term COMMAND PATTERN, which is what regular programmers
call methods-as-objects. The simplest explanation of a command pattern
is that a program uses objects to represent computational actions, such as
drawing a shape or changing the value of some field. Sophisticated programmers understand, however, that useful actions are parameterized over
arguments and often produce values, which is why our objects really represent something akin to functions, also known as closures in functional
programming languages such as Scheme.
Section 37
594
class C {
Type method1(. . . ) {
return method( new FunA(. . . ) ,. . . );
}
Type method1(. . . ) {
return
method(
new IFun<...>() {
public . . . invoke(. . . ) {
// a computation like methodA
}
})
}
Type method2(. . . ) {
return method( new FunB(. . . ) ,. . . );
}
private
class FunA implements IFun<...> {
public . . . invoke(. . . ) {
// a computation like methodA
}
}
private
class FunB implements IFun<...> {
public . . . invoke(. . . ) {
// a computation like methodB
}
}
}
Type method2(. . . ) {
return
method(
new IFun<...>() {
public . . . invoke(. . . ) {
// a computation like methodB
}
})
}
595
inition;
2. and anonymous implementations of interfaces.
In this section, we briefly study these mechanisms in general. They are relatively simple and easy to use, which we do in later sections of this chapter.
The left column in figure 203 is a reformulation of the classes in figure 202 as a single class. Nested within the class definition are two more
class definitions; see the framed boxes. A comparison of figure 203 with figure 202 shows that these nested class definitions are those we introduced
to re-create method1 and method2. Since these NESTED CLASSES are used
only for this purpose, they are intimately linked to class C, which is what
the textual nesting expresses and signals to any future reader. The figure
also shows that, just like methods and fields, classes come with privacy
attributes; we label classes used for command patterns with private.
While the nesting of the two auxiliary classes expresses their intimate
connection to class C, it does not reduce the textual overhead. For this
purpose, Java supports a mechanism for directly instantiating an interface
without first defining a class. In general, the direct instantiation of an interface has the following shape:
new IInterface () {
...
}
where the . . . define those methods that IInterface specifies. Naturally, if
IInterface is parameterized over types, you need to apply it to the correct
number of types, too: IIinterface<Type1, ...>. In either case, it is unnecessary to name a new class, which is why people speak of ANONYMOUS
INSTANTIATION.
Using the direct instantiation of interfaces, you can complete the last
step of the design recipe in a reasonably compact manner: see the right
column of figure 203. It shows that you can formulate the two methodsas-objects exactly where you need them. No future reader of this class
must search for auxiliary class definitionsglobal or nestedjust to find
out what the invoke methods compute. Instead, the code comes right along
with the re-definitions of the original methods.
The mechanism for instantiating interfaces directly facilitates the use of
this pattern but it is not essential. Indeed, the designers of many objectoriented programming languages have added constructs for creating firstclass functions directly because the instantiation of interfaces is still cumbersome. Because this book isnt about Java (alone), we use anonymous
Section 37
596
597
interface IVisitor<RESULT> {
RESULT visitC1(C1 x);
RESULT visitC2(C2 x);
...
}
The interface specifies one visit method per class that implements I.
If your chosen programming language supports overloading, you
may wish to specify an appropriate number of overloaded methods:
interface IVisitor<RESULT> {
RESULT visit(C1 x);
RESULT visit(C2 x);
...
}
For clarity, we forgo Javas overloading here.
3. to each variant C1, C2, . . . , add the following implementation of the
visit method:
inside of C1 :
public
<R>
R traverse(IVisitor<R> x) {
return x.visitC1(this);
}
inside of C2 :
public
<R>
R traverse(IVisitor<R> x) {
return x.visitC2(this);
}
...
...
...
...
...
...
Note: If you use overloading, the visit methods in all variants look
identical. Since Javas type checker must resolve overloaded method
calls before evaluation, however, it is impossible to apply the design
recipe of lifting those methods to a common superclass.
4. to each class C1, C2, . . . , you may also have to add getters for private
fields so that the visit methods may gain access as appropriate.
Equipping the data representation with such a traverse method ensures
that it invokes a visit method on each object that it reaches during a traversal. If the visit method chooses to resume the traversal via a call to traverse
for fields with self-referential types, the traverse method resumes its computation and ensures that visit is invoked on the additional objects. In short,
the terminology of visitor pattern is justified because the traverse method
Section 37
598
interface IList<I> {
<R> R traverse(IVisitor<I,R> f );
}
interface IVisitor<I,R> {
R visitMt(Mt<I> o);
R visitCons(Cons<I> o);
}
public
<R> R traverse(IVisitor<I,R> f ) {
return f.visitCons(this);
}
empowers others to specify just how many objects in a collection of interconnected objects must be visited and processed.
Figure 204 illustrates the result of adding a visitor-based traversal to our
conventional list representation. The top left is the list interface equipped
with a traverse signature; the top right shows the interface for a list visitor.
The implementing classesat the bottom of the figurecontain only those
methods that the above items demand.
Although the creation of the general traversal method itself doesnt require a design recipe, using the method does. Specifically, when you design
an implementation of the IVisitor interfacewhich we call a VISITORyou
are after all designing a class-based representation of a method, which we
also call a function object. Thus, using the visitor abstraction is all about
formulating a purpose for methods and functions, illustrating it with examples, designing a template, filling in the gaps, and testing the examples.
Lets enumerate the steps abstractly and examine them in the context of
the concrete list example:
1. The data definitions are already implemented; this step is of no concern to you when you are designing visitors.
2. Thus, the real first step is to formulate a contract and a purpose state-
599
ment. For visitors, you formulate it for the entire class, thinking of it
(and its instances) as a function that traverses the data. In the same
vein, the contract isnt a method signature; it is an instantiation of the
IVisitor interface at some type, requiring at a minimum the specification of a result type. Still, instantiating the interface determines the
functions contract, what it consumes and what it produces.
Example: Suppose you are to design a list traversal for integer lists
that adds 1 to each item on a list of integers. Describing the purpose
of such a function is straightforward:
// a function that adds 1 to each number on a list of integers
class Add1 implements IListVisitor<Integer,IList<Integer>> {
public Add1() { }
public IList<Integer> visitMt(Mt<Integer> o) { . . . }
public IList<Integer> visitCons(Cons<Integer> o) { . . . }
}
The signature is the instantiation of IListVisitor with a list item type
(Integer) and a result type (IList<Integer>).
Lets generalize the example a bit to adds or subtracts a fixed integer
to the items on the list. In the context of How to Design Programs, a
purpose statement and header for this problem would look like this:
;; addN : Number [Listof Number] [Listof Number]
;; add n to each number on a list of numbers
(define (addN n alon) . . . )
Note how the purpose statement refers to the parameter n, the number that is to be added to each list item.
Since the methods in the visitor interface are about one and only
one kind of argumentthe pieces of the data structure that is to be
traversedthere appears to be no room for specifying any extra parameters, such as n. At the same time, you know that of the two
arguments for addN, n is fixed throughout the traversal and alon is
the piece of data that addN must traverse. Hence we can turn n into a
field of the class:
Section 37
600
601
4. The templates for the visit methods is about the fields in the objects
on which the method is invoked (plus the fields of the visitor). To this
end you must know the fields in the classes of the data representation;
if fields are private, you must use the getter methods to access them.
For all fields in the visited object that refer back to the interface I, i.e.,
for all self-referential classes in the data representation, add
o.getField().traverse(this)
to the method body, assuming o is the parameter of the visit method.
This expression reminds you that the method may recur through the
rest of the data representation if needed.
This last point distinguishes a template for visit methods from a regular method template. While the two kinds of template are similar
in principle, the emphasis here is on the object that is being visited,
not the visitor object. The complete traversal is accomplished via an
indirect call to traverse (with this instance of the visiting class) not via
a recursive call to visit.
While you will use this for the recursive use of a visitor in most cases,
you will need different traversals to complete the processing on some
occasions. For an example, see below as well as the finger exercises
in the next two subsections.
Example:
inside of AddN :
IList<Integer>
visitMt(Mt<Integer> o) {
... n ...
}
inside of AddN :
IList<Integer>
visitCons(Cons<Integer> o) {
... n ...
. . . o.getFirst() . . .
. . . o.getRest().traverse(this) . . .
}
For lists you have just one class that refers back to the list representation (IList), so it is not surprising that only visitCons invokes traverse.
5. Now it is time to define the visit methods. As always, if you are
stuck, remind yourself what the various expressions in the template
computeusing the purpose statement if neededand then find the
proper way to combine these values.
Section 37
602
inside of AddN :
IList<Integer>
visitCons(Cons<Integer> o) {
int a = n + o.getFirst();
IList<Integer> r =
o.getRest().traverse(this);
return new Cons<Integer>(a,r);
}
603
checkExpect(m3.traverse(isPositive),false);
checkExpect(m2.traverse(isPositive),true);
Since m3 starts with 3, you should expect that the visitor can produce false without looking at the rest of the list, m2. In particular, because m2 contains only positive integersas the second test shows
traversing it contributes nothing to the final result of checking m3.
3. Given that instances of Positive consume the same kind of data as
AddN functions, we just adapt the template from the first example:
inside of Positive :
Boolean
visitMt(Mt<Integer> o) {
...
}
inside of Positive :
Boolean
visitCons(Cons<Integer> o) {
. . . o.getFirst() . . .
. . . o.getRest().traverse(this) . . .
}
4. The final method definitions look almost identical to the method definitions that you would have designed without traverse around:
inside of Positive :
public Boolean
visitMt(Mt<Integer> o) {
return true;
}
inside of Positive :
public Boolean
visitCons(Cons<Integer> o) {
return (o.getFirst() > 0) &&
(o.getRest().traverse(this));
}
In particular, the visitCons method first checks whether the first item
on the list is positive and traverses the rest only after it determines that
this is so. Conversely, if the invocation of getFirst produces a negative
integer, the traversal stops right now and here.
5. Complete the code and run the tests.
What this second example shows is that the design of a visitor according
to our design recipe preserves traversal steps. That is, the visitor steps
through the collection of objects in the same manner as a directly designed
method would. The abstraction works well.
The third example recalls one of the introductory exercises from How to
Design Programs on accumulator-style programming:
Section 37
604
interface IList {
// convert this list of relative
// distances to a list of absolutes
IList relativeToAbsolute();
}
}
class Mt extends AList {
public Mt() { }
. . . Design a data representation for sequences of relative distances, i.e., distances measured between a point and its predecessor, and sequences of absolute distances, i.e., the distance of
a point to the first one in a series. Then design a method for
converting a sequence of relative distances into a sequence of
absolute distances. For simplicity, assume distances are measure with integers. . . .
Figure 205 displays a complete solution for this problem. It uses a plain list
representation for both kinds of lists. The design is properly abstracted, locating the main method in an abstract class and pairing it with an auxiliary
(and protected) method based on accumulators.
While a solution like the above is acceptable after the first three chapters
of this book, a proper solution re-uses an existing list library and provides
the functionality as a visitor:
1. The purpose statement is the one from figure 205 and the class signa-
605
inside of RelativeToAbsolute :
IList<Integer>
visitCons(Cons<Integer> o) {
return
new Cons<Integer>(
o.getFirst()+ ??? ,
o.getRest().traverse( ??? ));
}
Section 37
606
The method in Cons should add the first distance to those that preceded it in the original list, and it should pass along the first distance
to the computations concerning the rest of the list. Put differently, the
function should accumulate the distance computed so far but doesnt.
Note: The method could also use AddN repeatedly to add the first distance of any sublist to the distances in the rest of the list. This solution
is, however, convoluted and needs too much time to evaluate.
Here is a second start:
1. Since the class is to represent an accumulator-style function, it comes
with an additional, constructor-initialized field:
class RelativeToAbsolute
implements IListVisitor<Integer,IList<Integer>> {
int dsf ; // distance so far
RelativeToAbsolute(int dsf ) {
this.dsf = dsf ;
}
public IList<Integer> visitMt(Mt<Integer> m) { . . . }
public IList<Integer> visitCons(Cons<Integer> c) { . . . }
}
Ideally this dsf field should be 0 initially but we ignore this detail
for the moment and assume instances are always created with new
RelativeToAbsolute(0).
2. The revised template is like the one for AddN except for the recursion
in Cons:
inside of RelativeToAbsolute :
IList<Integer>
visitCons(Cons<Integer> o) {
. . . o.getFirst() . . .
. . . o.getRest().traverse( new RelativeToAbsolute(. . . ) ) . . .
}
Instead of just using this, we indicate that a new instance of the class,
distinct from this, is possibly needed.
3. With this revision, working definitions are in plain sight:
607
inside of RelativeToAbsolute :
IList<Integer>
visitCons(Cons<Integer> o) {
int tmp = dsf + c.getFirst();
RelativeToAbsolute processRest = new RelativeToAbsolute(tmp);
return new Cons<Integer>(tmp,c.getRest().traverse(processRest));
}
4. To complete the first draft, it remains to test the visitor with the examples from above.
What also remains is to revise the draft class so that it doesnt expose a
constructor that may initialize the field to the wrong value.
Exercise
Exercise 37.3 Use privacy specifications and overloading to revise the first
draft of RelativeToAbsolute so that it becomes impossible to accidentally misuse it.
Finally, an implementation of IVisitor doesnt have to use concrete types;
it may use type variables, just like plain methods that traverse a piece of
data. Suppose you wish to design an implementation of IListVisitor that
acts like map from figure 192:
inside of IList<I> :
<RESULT> IList<RESULT> map(IFun<I,RESULT> f );
To do so, you must design a class whose result type is IList<RESULT> and
whose visit methods process items of type ITEM:
class Map<I,RESULT> implements . . .
Using the fresh type parameters, you can describe what kind of visitor the
instances of Map implement:
// process each item, collect in new list
class Map<I,RESULT>
implements IListVisitor<ITEM,IList<RESULT>> {
...
public IList<RESULT> visitMt(Mt<I> m) { . . . }
public IList<RESULT> visitCons(Cons<I> c) { . . . }
}
Section 37
608
Of course, these type choices are also reflected in the signatures for the visit
methods; they consume lists of ITEMs and produce lists of RESULTs. The
rest of this example is an exercise.
Exercises
Exercise 37.4 Reformulate the design instructions for visitor classes in a
world with just subtyping, i.e., without generics. Then formulate a design
recipe for visitors.
Exercise 37.5 Demonstrate the workings of your design recipe from exercise 37.4 with the definition of a general visitor pattern for list classes and
designs for the two visitor classes from this section: AddN and Positive.
609
4. Sort, which sorts a list of arbitrary objects. Parameterize the function over the comparison used; represent the comparison method as
an object that consumes two list items and produces a boolean. Define a single class though add nested classes as needed.
Exercise 37.8 Figures 23 and 24 (see pages 45 and 45) introduce a data representation for the representation of geometric shapes.
Equip this data representation with an abstract traversal method using
the visitor pattern.
Design In, a visitor that determines whether or not some position is
inside some given geometric shape.
Design Area, which computes the area of some given geometric shape.
Design BB, which constructs a representation of a bounding box for the
given shape. See section 15.3 for the definition of a bounding box.
interface ITree {}
class Leaf implements ITree {
int t;
Leaf (Object t) {
this.t = t;
}
}
Exercise 37.9 Figure 206 displays the class definitions for representing binary trees of integers. Equip this binary tree representation with an abstract
traversal, using the visitor pattern.
Design the visitor Contains, which determines whether a tree contains
some given integer.
Design Sum, which visits a binary tree and determines the sum of integers in the tree.
Exercise 37.10 Generalize the data representation of figure 206 so that it
represents binary trees of objects instead of just ints. Equip it with a visitor
Section 37
610
traversal and design the visitor class Contains for this binary tree representation (see exercise 37.9). Do not use generics for this exercise.
Exercise 37.11 Section 32.4 introduces two distinct data representations of
sets, one using lists (exercise 32.8) and another one using binary search
trees (exercise 32.14). Furthermore, the two exercises request the design
of generalizations using generics as well as subtyping, meaning you have
four different ways of representing sets.
Equip your favorite set representation with a visitor-based traversal
method so that you can design visitors that process all elements of a set.
To test your traversal method, design three visitors. The first one adds 1 to
each element of a set of integers, producing a distinct set of integers. The
second one uses the draw package to draw the elements onto a canvas. The
third one maps the elements of a set of integers to a set of corresponding
Strings.
3t
y
X
1 t
I
@
@
@ t
0
t4
interface IGraph {
// does this graph contain a node labeled i?
// effect: mutate the nodes of this graph
void addNode(int i);
// does this graph contain a node labeled i?
boolean contains(int i);
- t2
611
the fact that you can move from n to k in the world of information. If, in
this world, you can also move from k to n, you need to add an edge to the
graph that points from k to n.98
General speaking, a graph is an abstract rendering of many forms of
real-world information. For example, the nodes could be intersections of
streets in a city and the edges would be the one-way lanes between them.
Or, a node may stand in for a person and an edge for the fact that one
person knows about another person. Or, a node may represent a web page
and edges may signal that one web page refers to another.
As for a data representation of graphs, you should expect it to be cyclic.
Nothing in our description prevents nodes from referring to each other;
indeed, the description encourages bi-directional connections, which automatically create cycles. The IGraph interface on the right side of figure 207
follows from the design recipe of chapter IV. It suggests constructing basic
data first, which means the collection of nodes for graphs, and providing
a method for establishing connections afterwards, which for us means a
method for adding edges. Thus the graph library provides the interface
and a plain graph constructor:
inside of Graph implements IGraph :
public Graph() { }
Once you have a graph, you can specify the collection of nodes with the
addNode method and the collection of edges with the connect method.
Exercises
Exercise 37.12 Turn the information example from figure 207 into a data
example, based on the informal description of the library.
Exercise 37.13 Design an implementation of the specified library. Use a
generic list library, equipped with a visitor-based traversal, to represent the
collection of nodes and the neighbors of each node.
Constraint: Do not modify the list library for the following exercises.
Design a visitor for use with the list traversal method instead.
Exercises
98 We
are dealing here with directed graphs. In so-called undirected graphs, every edge
indicates a two-way connection.
612
Section 37
Exercise 37.14 Design a method for the graph library from exercise 37.13
that given the (integer) label of some node in a graph, retrieves the labels
of its neighbors.
Exercise 37.15 Design the interface IGraphNode, which represents nodes for
visitors. Then equip the graph library with a traverse method for processing the collection of nodes in a graph. Finally, design a visitor that computes the labels of all the nodes in a graph.
Exercise 37.16 Design an implementation of the graph visitor interface to
discover the sinks and sources of a graph.
A node i in a graph is a sink if there is no edge going from i to any other
node. In figure 207, the node labeled 3 is a sink.
A node o in a graph is a source if there is no edge from any other node
to o. In figure 207, the node labeled 0 is a source.
The node labeled with 4 is both a source and a sink.
As you may recall from How to Design Programs, a node i in a graph is
reachable from a node o if i = o or if there is a neighbor r of o from which i
is reachable. Thus, in figure 207 node 3 is reachable from node 0, because
node 2 is a neighbor of node 0 and node 3 is reachable from node 2. The
latter is true because node 3 is a neighbor of node 2. Note the recursive
nature of the description of the reachability process!
One way to compute the set of reachable nodes for any given node is to
start with its neighbors, to add the neighbors of the neighbors, the neighbors of those, and so forth until doing so doesnt add any more nodes.
Proceeding in this manner is often called computing the transitive closure
(of the neighbor relation) for the node.
Exercises
Exercise 37.17 Design the method reachable. Given a node n (label) in a
graph, the method computes the list of nodes reachable from n.
Exercise 37.18 Design a graph visitor that computes for every node the list
of all reachable nodes. Use the solution of exercise 37.17.
A mathematicians description of graphs doesnt usually use lists but
sets. The former emphasizes the existing of an sequential arrangement
613
among objects and allows for repetitions; the latter assumes no ordering
and allows no repetition of elements. Thus, when we suggested the use of
lists for the collection of nodes in the graph or for the collection of neighbors per node, we committed to an ordering where none seems needed.
Exercise
Exercise 37.19 Inspect all the method signatures in IGraph and IGraphNode,
including the ones you have added. For all occurrences of IList<T> decide
whether it makes sense to replace it with ISet<T>. In other words, determine whether repetition of items matters and whether the sequencing of
the item matters.
Re-design the methods using the interface from section 32.4. First use
the set library based on lists, then use the one based on binary trees. Do
you have to change any of your code when you switch?
+--------------+
| UFOWorld
|
+--------------+
* +------+
| ????? shots |----->| Shot |
+--------------+
+------+
+------+
Section 37
614
A second look at the problem suggests that the method for dropping expired tasks must traverse the collection of Tasks. We have therefore added
a signature for such a task to the box for TaskQueue.
615
For now assume that, like in section 32.4, queues contain a list of Tasks,
represented via the list library from figure 204:
class TaskQueue {
public TaskQueue() { }
private IList<Task> tasks = new Mt<Task>();
private int howMany = 0;
...
// effect: remove those tasks from this queue that are expired
public void expireTasks() {
. . . tasks . . . howMany . . .
tasks . . .
howMany . . .
return ;
}
The class fragment also spells out the first three steps of the design recipe
for the expireTasks method: its purpose and effect statement, its signature,
and its template. The latter reminds you that the method may use the values of the two (private) fields and that it can change them.
Skipping the examples step for now, we turn to the method definition
step. Given that the TaskQueue refers to its collection of tasks via a list, the
method should traverse this list, eliminate the expired tasks, and retain the
remaining ones. Describing in this manner suggests the design of a visitor
for the list library:
inside of TaskQueue :
public void expireTasks() {
tasks = tasks.traverse(new ByDate());
howMany = . . .
}
// select those tasks from the list that havent expired yet
private class Expired implements IListVisitor<Task,IList<Task>> {
public Expired() { }
public IList<Task> visitMt(Mt<Task> this0) { . . . }
public IList<Task> visitCons(Cons<Task> this0) { . . . }
}
Following the advice from section 37.4, this auxiliary class is nested and
hidden with TaskQueue because its methods are the only one that use it.
Section 37
616
As you can see from the purpose statement of the visitor, it determines
for each Task object on the list whether it is expired and retains those that
arent. Since expired typically means with respect to some time, the Expired class should probably grab the current data and/or time:
// select those tasks from the list that havent expired yet
class Expired implements IListVisitor<Task,IList<Task>> {
private Date today = . . .
private Time now = .
public Expired() { }..
public IList<Task> visitMt(Mt<Task> this0) { . . . }
public IList<Task> visitCons(Cons<Task> this0) { . . . }
}
We leave it to you to complete the design from here. The point is that
you have seen that the traversal is used in TaskQueue and that therefore the
visitor class is nested with TaskQueue.
Now take a look at this second problem:
. . . You are to design an electronic address book for a small
company. On request, the check-out clerks enter a customers
name, email, and zip code into the address book; the cash register adds what the customers have bought. The company uses
this address book for customer assistance (returns, information
about purchased items) but also for sending coupons for select
stores to customers. . . .
Here the problem implies that an address book is an aggregation of customer records:
+-----------------------+
| AddressBook
|
+--------------+
+-----------------------+
|
*| Customer
|
customers
|--------->+--------------+
+-----------------------+
| String name |
|
...
|
| String email |
| IList<Customer>
|
| String zip
|
|
select(String z)
|
| ...
|
|
...
|
+--------------+
+-----------------------+
How the AddressBook class is associated with its collection of Customers remains unspecified. The desire to look up individual customers quickly
while they waitimplies that a binary search tree is a better choice than
a plain list. No matter which choice you make, however, you do need a
method for selecting the customers with some specific zip code so that the
company can mail coupons to specific regions. Just as before this select
617
method traverses the entire collection of Customers and retains those with
the given zip code attribute. Thus, select uses traverse and, to this end, you
must place a visitor class in AddressBook.
Exercises
Exercise 37.20 Design an AddressBook, including the select method, using
the list library from figure 204. Also add a method addCustomers, which
consumes a list of Customers and adds them to the AddressBook.
Exercise 37.21 Design an AddressBook, including the select method, using
the general binary search tree representation that you designed for exercise 32.13 (page 507). Start by adding a traverse method to the library. Dont
forget to add a method addCustomers, which consumes a list of Customers
and adds them to the AddressBook.
Section 37
618
Visitors
pre-existing traverse
class purpose & interface signature
functional examples
visit methods in interface
visit per implementing class
access methods & calls to traverse
coding: start with base cases
coding: connecting expressions
testing examples
619
100 You
may wonder whether there is Void related to void like Integer is related to int. While
Java comes with a class Void, its role is different than Integers, and it has no use here.
Section 38
620
inside of UFOWorld :
// move all objects in this world
public void move() {
ufo.move();
aup.move();
shots.move();
charges.move();
return ;
}
For simplicity, this move method assumes that the methods it invokes consume no additional arguments.
The method definitions for moving the Shots on a list look like this:
inside of MtShot :
void move() {
return ;
621
inside of ConsShot :
void move() {
first.move();
rest.move();
return ;
}
The method on the left performs no computation at all; the method on the
right invokes the move method on the first instance of Shot and recurs on
the rest of the list.
If you were to look back at other imperative methods that process lists,
you would find that this arrangement is quite common, which is why you
want to abstract it. Roughly speaking, the abstraction is just like the map
method with two exceptions. First, it uses methods-as-objects that are imperative and have return type void. Second, the results of processing the
first item and traversing the rest of the list are combined by sequencing the
two effects, throwing away the results of the first computation.
Figure 210 displays the complete design of a general and imperative
traversal method for lists. The method is dubbed forEach, implying that it
performs some action for each item on the list. Otherwise the design has
the expected elements:
1. The IAction interface specifies the shape of methods as objects that the
forEach method consumes.
2. The IList interface includes the signature for the forEach method, indicating that forEach consumes an IAction<I> whose second argument
has type I, the type of a list item.
3. The two implementations of IList define forEach in the manner discussed. Specifically, the method in Mt performs no action; the method
in Cons invokes the action on first and then recurs on rest.
In short, forEach really is closely related to map and less so to traverse. Before you proceed with your readings, you may wish to consider in which
situations you would rather have a traverse style method.
Section 38
622
drawing objects from the UFOWorld class, because it aggregates the Shots
and other collections of objects.
Lets start with an action for moving all the shots Shot. Keep in mind
that the instances of the class represent methods that are invoked on every
Shot on the list, one at a time:
1. The purpose statement is just that of the move method in Shot:
inside of UFOWorld :
// move the shots on this list
private class Move implements IAction<Shot> {
public void invoke(Shot s) { . . . }
}
2. Just like for the design of any method, you need examples. In the
case of imperative methods you need behavioral examples. That is of
course also true if you represent methods as objects:
inside of Examples :
IList<Shot> mt = new Mt<Shot>();
IList<Shot> l1 = new Cons<Shot>(s1,mt);
IList<Shot> l2 = new Cons<Shot>(s2,l1);
IList<Shot> l3 = new Cons<Shot>(s3,l2);
IList<Shot> m1 = new Cons<Shot>(t1,mt);
IList<Shot> m2 = new Cons<Shot>(t2,m1);
IList<Shot> m3 = new Cons<Shot>(t3,m2);
l3.forEach(new Move()) ;
. . . checkExpect(l3,m3) . . .
The gray box highlights the invocation of forEach on l3 using an instance of Move. Below the box, a checkExpect expression formulates
our expectation that forEach changes l3.
3. Like the template for all imperative methods, the one for invoke suggests that the method may use its arguments (this, s) and the fields of
its invocation object (none):
623
inside of Move :
public void invoke(Shot s) {
... s ...
return ;
}
4. Filling in the template leaves us with the expected invocation of move
on the given shot:
inside of Move :
public void invoke(Shot s) {
s.move();
return ;
}
5. Finally its time to run the tests.
With Move in place, you can now move the shots in UFOWorld via an
invocation of forEach:
inside of UFOWorld :
public void move() {
ufo.move();
aup.move();
shots.forEach(new Move());
charges.move();
return ;
}
inside of UFOWorld :
private IAction<Shot> mS = new Move();
public void move() {
ufo.move();
aup.move();
shots.forEach(mS);
charges.move();
return ;
}
On the left you see the most direct and notationally most concise way of
doing so. On the right you see the best version from a computationally
perspective. Specifically, while the version on the left creates one instance
of Move per tick event, the version on the right instantiates Move only once
for the entire world. Although it is unlikely that a player or viewer can tell
the difference, it is important for you to begin to appreciate this difference.
For a second design example, consider the case of drawing a list of
Shots. Remember that draw in UFOWorld is called to refresh theCanvas,
which comes with every World:
Section 38
624
inside of UFOWorld :
// draw all objects in this world onto theCanvas
public void draw() {
drawBackground();
...
shots.draw(theCanvas);
...
}
The code snippet ignores that the draw methods actually consume the World
so that they can find out its dimensions. From here, we proceed as before:
1. The purpose statement and the contract are straightforward again:
inside of UFOWorld :
// draw a shot to the given canvas
private class Draw implements IAction<Shot> {
public void invoke(Shot s) { . . . }
}
Like Move, Draw implements IAction<Shot>. Its invoke method processes one shot at a time. We know, however, that draw methods always consume a Canvas into which they draw a shape. As with AddN
use a field to store the chosen Canvas throughout the entire traversal:
inside of UFOWorld :
// draw a shot to the given canvas
private class Draw implements IAction<Shot> {
private Canvas can;
public Draw(Canvas can) {
this.can = can;
public void invoke(Shot s) { . . . }
}
2. The template for invoke lists both a parameter and a field:
inside of Draw :
public void invoke(Shot s) {
. . . s . . . this.can . . .
return ;
}
625
inside of UFOWorld :
private IAction<Shot> dS =
new Draw(theCanvas);
public void draw() {
drawBackground();
...
shots.forEach(dS);
...
}
We show both solutions again, the notational concise version as well as the
one that creates only one object to represent the method.
As you can see, designing actions for imperative traversals is straightforward. Following the design recipe is too much work for such nonrecursive methods, also because you (should) have internalized it all. Even
the notational overhead seems high; you add private classes and instantiate them, even if just once. If you recall How to Design Programss lambdadefined functions, you sense that there should be an easier way. Fortunately, there is.
Exercises
Exercise 38.1 Use the list library from figure 210 to design the method
drawAll. The method consumes a list of Posns and draws them as red dots
on a 200 by 200 canvas. Add the method to an Examples class.
Exercise 38.2 Use the list library from figure 210 to design the method
swap. The method consumes a list of Posns and imperatively swaps the
x with the y coordinates in each Posn.
Section 38
626
class C {
...
. . . new FunII() . . .
...
}
627
That is, new is useful in conjunction with an interface I if the new I() is
followed by a block that defines the specified methods of I.
Notes: Since it is impossible to define a constructor for an anonymous
classwe dont even have a name for ityou need to initialize the fields of
an anonymous interface implementation directly. As you do so, you may
wish to use local fields, which is legal in Java, or local variables, which isnt
(immediately) legal in Java, though in other object-oriented languages. Because of the Java specificity of this issue, we recommend that you look up
the exact rules and mechanisms when you are to design lasting programs
in Java. Until then, keep in mind that it is about design principles not language details.
When you are dealing with generic interfaces, the anonymous implementation must also specify type arguments for the type parameters. Here
is, for example, an anonymous instantiation of IAction:
new IAction<Shot> () {
public void invoke(Shot s) {
s.move();
return ;
}
}
Because the interface is parameterized over the type of list items that its
invoke method must process, the generic interface is applied to one type,
Shot in this case. Furthermore, because the interface specifies one method
signature in terms of its type parameter, the anonymous implementation
consists of a block with a single method on Shots.
As you can easily tell, both Move (for Shot) and Draw (also for Shot)
occur once in UFOWorld . Hence these classes are candidates for replacing
them with anonymous implementations and instantiations. Indeed, doing
so is relatively easy; it is just like the above, abstract example:
Section 38
628
inside of UFOWorld :
public void move() {
ufo.move();
aup.move();
shots.forEach(
new IAction<Shot> () {
public void invoke(Shot s) {
s.move();
return ;
}
}
);
charges.move();
return ;
}
inside of UFOWorld :
public void draw() {
drawBackground();
ufo.draw(theCanvas);
aup.draw(theCanvas);
shots.forEach(
new IAction<Shot> () {
public void invoke(Shot s) {
s.draw(theCanvas);
return ;
}
}
);
charges.draw(theCanvas);
return ;
}
The one aspect worth a remark is the reference to theCanvas from the anonymous instantiation of IAction in the draw method. Because theCanvas is a
field in the surrounding classindeed, a field in the superclassthis reference is legal.
Lets look at one last example, the creation of a complete card deck.
Card games exist in many different cultures and come in many different
forms. All of them, though, involve a deck of cards, and cards belong to a
suit and have a rank. Figure 211 summarizes the scenario in those terms.
The Game class corresponds to the World class with which we always
start to lay out what we have. It sets up the legal suits and ranks for the
game, both individually and as lists. The next field creates a deck. The rest
is left to a setUp method, which is presumably responsible for creating the
deck, shuffling it, dealing the cards to players, and so on. Its first action
is to invoke createDeck, which is to add cards of all (specified) suits at all
(specified) ranks to the initially empty deck. All of this is captured in the
methods purpose and effect statement.
Our goal here is to design this createDeck method. With the purpose
statement given, we can move straight to the examples, which is best done
with a table here:
Clubs
Diamonds
...
Seven
...
...
...
Eight
...
...
...
Nine
...
...
...
...
...
...
...
Ace
...
...
...
class Game {
Suit d = new Suit("Diamond");
Suit c = new Suit("Clubs");
...
Rank seven = new Rank("seven");
...
Rank ace = new Rank("ace");
IList<Suit> s0 = new Mt<Suit>();
IList<Suit> s1 = new Cons<Suit>(d,s0);
...
IList<Suit> suits = new Cons<Suit>(c,. . . );
IList<Rank> r0 = new Mt<Rank>();
IList<Rank> r1 = new Cons<Rank>(seven,r0);
...
IList<Rank> ranks = new Cons<Rank>(ace,. . . );
Deck deck = new Deck();
...
public void setUp() {
this.createDeck();
...
}
629
class Card {
Suit s;
Rank r;
// constructor omitted
}
class Rank {
String v;
// constructor omitted
}
class Suit {
String s;
// constructor omitted
}
class Deck {
private IList<Card> listOfCards = new Mt<Card>();
public Deck() { }
The first row specifies some of the possible ranks, the first column lists the
possible suits. For each pairing of suits and ranks, the createDeck method
must create a card and add that card to the deck. The problem statement
implies that the order appears to be irrelevant.
Section 38
630
Both the purpose statement and the examples suggest that the method
must traverse ranks as well as suits. Following the advice in How to Design
Programs, we should consider three cases: processing one while treating the
other as a constant; processing both in parallel; and processing the cross
product. The table suggests the last option, meaning the method should
traverse one list and then, for each item on that list, the other.
At this point, you guess and choose one of the lists as the primary list
for iteration. If the design fails, you choose the other order. Lets start with
suits for now and lay out the template for forEach:
void createDeck() {
suits.forEach(new IAction<Suit> () {
public void invoke(Suit s) {
. . . s . . . ranks . . .
return ;
}
});
return ;
}
This template is dictated by the choice to traverse suits with forEach and the
shape of IAction<Suit>. Concretely, the two say that you must design an
invoke method and that invoke is applied to one suit at a time.
The templates body tells us that s and ranks are available. From the example step we know that the method is to traverse the list of ranks and pair
each with s. Put differently, invoke must traverse ranks. Since a traversal
within a traversal sounds complex, the proper decision is to design an auxiliary method or actually class, because methods are represented as objects:
void createDeck() {
suits.forEach(new IAction<Suit> () {
public void invoke(Suit s) {
ranks.forEach(new PerRank(s));
return ;
}
});
return ;
The full method definition on the left assumes that we can design the auxiliary method. For the latter, the partial class definition on the right shows
631
how much we know: the purpose and effect statement, the outline of the
class, and that it needs a field to keep track of the current suit (s).
Designing PerRank proceeds just like the design of any method. For
examples, we can take examples for createDeck and formulate examples for
PerRank. More precisely, PerRank holds the suit constant and looks at all
ranks, meaning it is one row of the above table. To translate this into a
template, we sketch out the body of the invoke method:
incPerRank
public void invoke(Rank r) {
... s ... r ...
}
where s is the chosen suit and r is the rank that the method is currently
processing. With all this information laid out, you can translate the purpose
and effect statement into a full method definition:
void createDeck() {
suits.forEach(new IAction<Suit> () {
public void invoke(Suit s) {
ranks.forEach(new PerRank(s));
return ;
}
});
return ;
For completeness, we re-state the definition of both the main method, createDeck, as well as the full definition of the auxiliary method. Figure 212
summarizes the design.
Section 38
632
class Game {
IList<Suit> suits = new Cons<Suit>(c,. . . );
...
IList<Rank> ranks = new Cons<Rank>(ace,. . . );
Deck deck = new Deck();
...
public void setUp() {
this.createDeck();
...
}
// effect: add all suits at all ranks to the deck
private void createDeck() {
suits.forEach(new IAction<Suit> () {
public void invoke(Suit oneSuit) {
ranks.forEach(new PerRank(oneSuit));
return ;
}
});
return ;
}
// effect: add all ranks of a given suit to the deck of this game
private class PerRank implements IAction<Rank> {
Suit s;
PerRank(Suit s) { this.s = s; }
public void invoke(Rank r) {
deck.addCard(new Card(s,r));
return ;
}
}
design is just that: a draft. Dont forget to use one method per task, and
dont forget to use the recipes for abstraction to edit your programs.
Exercises
Exercise 38.4 Equip the list library from figure 210 with a visitor-based
traversal method.
633
Exercise 38.5 Re-design your War of the World project. Start from a version of the game that allows the UFO to drop charges on a random basis
(see exercise 19.20, page 284).
Exercise 38.6 Re-design the imperative version of your Worm project.
See section 19.9 for the original case study and exercise 27.16 (page 408) for
the development of the imperative version.
Exercise 38.7 The interactive computer game Fire Plane is about extinguishing wild fires, a nuisance and occasionally a serious danger in the
western states of the US and in many other countries, too.101 Imagine a
prairie with fires flaring up at random places. The player is in control of a
fire plane. Such an airplane has water tanks that it can empty over a fire,
thus extinguishing it.
Design a minimal Fire Plane game:
1. Your game should should display one fire when the game starts.
2. While any fire is burning, your game should add other fires at random places. The number of fires should be unlimited, though, you
should be careful not to start too many fires at once.
3. Also, your game should offer the player one fire plane with a fixed
number of water loads. The fire plane should move continuously.
You are free to choose the means by which the player controls the
movements of the plane and which movements it may perform.
101 We
thank Dr. Kathi Fisler for the idea of the fire-fighting game.
Section 38
634
635
Section 38
636
library. If you try the latter, you must figure out how to define sum with
forEach instead of traverse:
inside of Examples :
// determine the sum of l
public int sumFE(IList<Integer> l) {
l.forEach(new IAction<Integer> () {
public void invoke(Integer i) {
... i ...
return ;
}
});
return . . . ;
}
Given that forEach produces no result, the invocation of the method on l
cant be the last part of the method. Similarly, invokes return type is also
void, meaning it too cant produce a value via a plain return. Thus the
first conclusion is that we need an stateful field102 and an assignment to
this field in invoke to communicate the sum from inside of invoke to its surroundings:
inside of Examples :
private int sumAux = 0;
public int sumFE(IList<Integer> l) {
l.forEach(new IAction<Integer> () {
public void invoke(Integer i) {
... i ...
sumAux = . . .
return ;
}
});
return . . . ;
}
The new field is called sumAux because it is associated with sumFE. Also
note that, following the design recipe for imperative methods, invoke now
comes with a partial assignment statement to the new field.
The template is suggestive enough to make progress. If invoke adds i to
sumAux for each item on the list, sumAux should be the sum of all integers
102 We
cant use a local variable that is hidden inside of sumFE due to Javas restrictions.
See the note on this issue in the preceding section.
637
Ran 2 tests.
1 test failed.
...
actual: 7
expected: 6
The second test fails, because sumFE returns 7 when 6 is expected.
Stateful classes and fields are tricky, and we have just been bitten. While
the sumAux field is initialized to 0, the sumFE method just keeps adding to
sumAux never re-setting it when it is done with a list. There are two obvious
solutions: one is to set sumAux to 0 before the list is traversed and another
is to set it to 0 afterwards. The first solution is simpler to write down than
the second one and easier to comprehend:
Section 38
638
inside of Examples :
private int sumSoFar;
public int sumFE(IList<Integer> l) {
sumSoFar = 0;
l.forEach(new IAction<Integer> () {
public void invoke(Integer i) {
sumSoFar = sumSoFar + i;
return ;
}
});
return sumSoFar ;
}
A close look at this definition shows that initializing the field to 0 at the very
beginning has two advantages. First, the initialization and the assignment
in the forEach traversal show the reader that the purpose of the field is to
represents the sum of the integers encountered so far. We have therefore
renamed the field to sumSoFar. Second, the initialization avoids accidental
interferences, just in case some other method uses the field.
Lets consider a second example, selecting all positive numbers from
a list. Since the problem statement contains a perfectly phrased purpose
statement, we just add a contract to get started:
inside of Examples :
// select all positive integers from the given list
public IList<Integer> allPositiveV(IList<Integer> l) { . . . }
The examples remind you of two points. First, allPositive isnt a method (of
the integer list representation), but a function-like construction. Second,
because 0 isnt positive, the function drops it from both i1 and i2.
Furthermore, the two examples fail to explore the problem statement
properly. Although the design of proper examples and tests is a topic for a
639
book of its own, this sample problem demands a third test, one that can tell
whether the function-method preserves the order of the numbers:
inside of Examples :
IList<Integer> i3 = new Cons<Integer>(2,i2);
IList<Integer> o2 = new Cons<Integer>(2,o1);
. . . checkExpect(allPositive(i3),o2) . . .
While the preservation of order is not mentioned in the problem statement,
youthe problem solvershould wonder about it and writing down an
example shows that you did.
At this stage in your development, you can skip the template and write
down the definition:
inside of Examples :
// select all positive integers from the given list
public IList<Integer> allPositiveV(IList<Integer> l) {
return l.traverse(
new IListVisitor<Integer,IList<Integer>> () {
public IList<Integer> visitMt(Mt<Integer> o) {
return new Mt<Integer>();
}
public IList<Integer> visitCons(Cons<Integer> o) {
int f = o.getFirst();
if (f > 0) {
return new Cons<Integer>(f ,o.getRest().traverse(this)); }
else {
return o.getRest().traverse(this); }
}
});
}
Given that the purpose of the method is to traverse the given list and to
make a decision for each integer on the list, it is natural to invoke the traverse method and to use an anonymous implementation and instantiation
of the IListVisitor interface. This visitors first method returns an empty list
for a given empty list, and its second method inspects the first integer before it decides whether to add it to the result of traversing the rest.
Following the first example, we can obviously abuse forEach in a similar
way, using a (private) field to keep track of the positive integers so far:
Section 38
640
inside of Examples :
// select all positive integers from the given list
private IList<Integer> posSoFar;
public IList<Integer> allPositiveFE(IList<Integer> l) {
posSoFar = new Mt<Integer>();
1
l.forEach(new IAction<Integer>() {
public void invoke(Integer f ) {
if (f > 0) {
posSoFar = new Cons<Integer>(f ,posSoFar);
return ; }
else {
return ;}
}
});
return posSoFar;
Like the definition of sumFE, the one for allPositiveFE consists of three parts,
each highlighted in gray and labeled with a subscript:
1. The first part initializes the auxiliary private field to the proper value.
Here this means an empty list, because no positive integer has been
encountered so far.
2. The second part is an imperative traversal based on forEach. For each
positive integer encountered, the invoke method updates the private
field via an assignment statement. It thus adds the most recently encountered positive number to the list of positives seen so far before
forEach continues to process the list.
3. The third part of the method returns the current value of the field.
Our description says that allPositiveFE proceeds like sumFE. While the latter
starts with 0 as the sum seen so far and adds integers as it encounters them,
allPositiveFE starts with an empty list and adds positive integers.
This last thought suggests that the results of allPositiveFE and allPositive
differ in the order in which the numbers appear on the result list. And
indeed, adding appropriate examples and tests spells out and confirms this
difference in plain view:
641
. . . checkExpect(allPositive(i3),
new Cons<Integer>(2,new Cons<Integer>(1,mt)) . . .
. . . checkExpect(allPositiveFE(i3),
new Cons<Integer>(1,new Cons<Integer>(2,mt)) . . .
The two examples suggest that methods based on forEach traversals are
accumulator versions of their traverse-based counterparts. You may wish to
re-visit How to Design Programs, chapter VI, to refresh your memory of the
differences between naturally recursive functions and their accumulatorstyle counterparts. We continue to explore the topic, as well as the abuse of
forEach, in the exercises.
Exercises
Exercise 38.10 Design accumulator-based variants of sum and allPositive.
In other words, design applicative visitor classes and use instances of these
visitors in conjunction with sum and allPositive.
Exercise 38.11 While allPositiveFE and allPositive obviously produce different results, this doesnt appear to be true for sumFE and sum. Can you think
of a list of integers for which the two methods would produce different results? How about doubles?
Exercise 38.12 Design two variants of juxtapose, which consumes a list of
Strings and computes their juxtaposition. The first uses traverse to accomplish its purpose, the second uses forEach. Do their results differ for the
same input?
Exercise 38.13 Design two variants of the method min, one using traverse
and the other using forEach. The method consumes a non-empty list of ints
and determines the minimum.
Exercise 38.14 Design two variants of the method closeTo, one using traverse and the other using forEach. The method consumes a list of Posns and
determines whether any one of them is close to the origin. For the purpose
of this exercise, close means a distance of less or equal to 5 (units). Does
one variant have an performance advantage over the other?
Exercise 38.15 Use forEach to design the method sort, which consumes a
list of ints and produces one sorted in ascending order.
Section 39
642
LIBRARY
+-----------+
+----------+
*
| IIfc
|
| IVisitor |
*
+-----------+
+----------+
*
|
|
*
/ \
/ \
*
----*
|
FUNCTIONS
*****************|*****************
+---------------+----------------+-------+ *
+--------+--------+------------+
|
|
|
| *
|
|
|
|
+-----------+
+-----------+
+-----------+ | * +--------+
+--------+
+----------+
| ACls
|
| BCls
|
| CCls
| | * | Visit1 |
... | Visitn |
| Visitn+1 |
+-----------+
+-----------+
+-----------+ | * +--------+
+--------+
+----------+
| *
* * * * * * * * * * * * * * * * * * * * * * * * | *
DATA EXTENSION
|
|
+-----------+
| DCls
|
+-----------+
103 See
643
644
Intermezzo 6
TODO
purpose: to apply programming via refinement in the Java context
VII
Java suffers from a serious design flaw that impinges on proper objectoriented design.104
it is not just tail-recursion, there is something else too (i wish i could
recall)
and
void for(...)
is the only loop that Java provides. this forces a programmer to write
imperative code almost all the time, even though Java by design is an
object-oriented language otherwise.
Steele, a co-specifier of the language, has repeatedly agreed with this statement in
public, the last time during a talk in Northeastern Universitys ACM Curriculum Series.
Section 44
646
}
})
}
})
for (Suit suit : suits)
for (Rank rank : ranks)
sortedDeck.add(new Card(suit, rank));
Loops
647
Intermezzo 7: Loops
syntax
typing and subtyping
BUT semantics: compile to the Object based solution with subtyping
error messages are weird:
648
Intermezzo 7
TODO
purpose: some more Java linguistic (static, packages); some Java lib stuff
(collections, stacks); some Java GUI stuff (world, if you dont have our libraries)
VIII
49 Java doc
Java
Section 49
650
To CHECK
Developing Large Programs
Iterative Refinement
Layers of Data Abstraction
1. AM I INTRODUCING OVERLOADED METHODS PROPERLY?
2. AM I INTRODUCING OBJECT as SUPER before V?
3. WHEN do I drop this?
Java doc
651
Most of the time (eg. Fig 30), a class with no fields is drawn with
single underline:
+---------+
| MTShots |
+---------+
But sometimes they are drawn with a double underline, as in Fig
31:
+------+
| Blue |
+------+
+------+
(e) code: add(?) information about the package imports to worldish figures
6. cross check all Designing subsections for style/content
7. check on examples clases, should they have 0-ary constructors?
Section 49
652
To Do
1. make sure to say how to create instances of Object at the end of chapter 4 when Object is introduced
2. part should be chapter
3. should stub definitions replace headers in the design recipe?
4. else { should be on a line by itself. The whole if { } else { } type setting
requires a close look.
5. templates shouldnt have comments
6. CHANGE RECIPE SO THAT STUDENTS USE THE TYPES OF THE
INVENTORY ITEMS
7. ?? replace template with inventory, plus footnote ??
8. use natural recursion a lot in chapters 2 and forth
9. drop this. from iv on up
10. add exercises to the section on method dispatch and type checking
11. ProfessorJ Companion Guide (appendix? on-line?)
12. Eclispe transition as a separate guide (appendix? on-line?)
Java doc
Unallocated Goals
653
654
File allocations
Robby: 1.tex 1-2.tex 2.tex 1.tex 2-3.tex 3-4.tex 3.tex
Matthias: 4-5.tex 4.tex 5.tex 6.tex 7.tex
Section 49