Understanding the Code
After Jamie introduced the four classes (driver
,
Simulation
controller, BoundedGrid
,
and Fish
) that form the conceptual core of the MBS design, I
mentioned that I had already looked at the code for the
SimpleMBSDemo2
driver and Simulation
classes. I
felt ready to move on, but Jamie just laughed and suggested we look at the
real driver: MBSApp
. I was about to discover what that
laughter was about.
The MBSApp
Class
Read the code for MBSApp.java
as you read this section!
I had already noticed that SimpleMBSDemo2
had slightly less
code than SimpleMBSDemo1
, but I wasn't prepared for the sight
of MBSApp
. This class has only four lines of code, none of
which construct a grid, a Simulation
object, nor any fish.
Jamie explained that all that this driver does is set up the graphical user
interface.
Its first two statements combine to specify what types of objects can be
put in the grid (only Fish
objects, for now), the third
says how to display a fish, and the fourth finally constructs the user
interface.
The user interface then constructs the grid and fish when the user chooses an
initial configuration file or constructs and edits a grid manually. It
also plays the role of the simulation controller, managing the
run
and step
functions in response to button
clicks on the user interface.
The graphical user interface provides nice functionality for the
simulation, but I was glad that I had looked at the
SimpleMBSDemo2
driver and its Simulation
class
first, so I could better understand the overall program logic.
Exercise: MBSApp
- Experiment with the other fish display classes mentioned in
MBSApp
.- Experiment with creating and populating new grids manually, if you haven't already. What do you think is the connection between the "grid editor" window (adding new objects manually) and the code in
MBSApp
?
The BoundedGrid
Class
(Grid Package)
Now that I had looked at both the SimpleMBSDemo2
and
MBSApp
drivers, I was ready to learn more about
BoundedGrid
and Fish
. The code for the
BoundedGrid
class is buried in the Grid Package, though, so it
would be a "black box" for me.
The best reference for the Grid Package classes, including the
BoundedGrid
class, is the class documentation.
To get me started, Jamie gave me a list of useful BoundedGrid
methods, including the constructor I had seen used in the demo programs.
These methods are also documented on the MBS Quick Reference Sheet.
public BoundedGrid(int rows, int cols)
| Constructs an empty grid with the specified dimensions. |
Accessor methods dealing with grid dimensions, locations, and navigation | |
public int numRows()
| Returns number of rows in grid. |
public int numCols()
| Returns number of columns in grid. |
public boolean isEmpty(Location loc)
| Returns true if loc is a
valid location in this grid and is empty; otherwise returns
false .
|
public boolean isValid(Location loc)
| Returns true if loc is a
valid location in this grid; otherwise returns
false .
|
public Direction randomDirection()
| Generates a random direction. |
public Direction getDirection(Location fromLoc, Location
toLoc)
| Returns the direction from one location to another. |
public Location getNeighbor(Location fromLoc, Direction
compassDir)
| Returns the adjacent neighbor of a location in the specified direction. |
public ArrayList
| Returns a list of the neighbors to the north, south, east, and west of the given location |
Methods to add/retrieve/remove objects | |
public void add(GridObject obj, Location loc)
| Adds the specified object to the grid at the specified location. |
public GridObject objectAt(Location loc)
| Returns the object at location loc
|
public void remove(Location loc)
| Removes whatever object is at the specified location in this grid. |
Some of these methods are used by the driver to construct the environment,
some (such as numRows
, numCols
,
objectAt
) are used by the graphical user interface to display
the contents of the grid, and some (such as neighborsOf
,
isEmpty
) are used by the Fish
class when fish are
attempting to move.
The Location
and
Direction
Classes (Grid Package)
The Location
class, commonly used with a
BoundedGrid
, provides a way to specify the row and column of a
cell in the grid. The Location
constructor takes a row and a
column as parameters and creates a single object encapsulating them, while
the row()
and col()
methods provide access to the
individual attributes.
public Location(int row, int col)
| Constructs a location object with the specified row and column. |
public int row()
| Returns the row associated with this location. |
public int col()
| Returns the column associated with this location. |
The Direction
class represents compass directions,
providing several constants, such as Direction.NORTH
and
Direction.SOUTH
, and a small group of methods.
Analysis Questions: Location and Direction
(A Markdown template for writing up answers to these Analysis Questions is here.)
- Look over the list of 15
edu.kzoo.grid
classes in the Grid Package class documentation. (You will have to click on theedu.kzoo.grid
package name in the upper-left panel to bring the number of classes down from 75 to 15.) The Marine Biology Simulation primarily uses theBoundedGrid
,Location
,Direction
, andGridObject
classes. Which other classes have you used in previous labs and projects?- Look at the class documentation for the Grid Package
Location
class. It has several other methods, in addition to the ones mentioned by Jamie. What do theequals
andcompareTo
methods do? What about thetoString
method? (You could run the simulation program and hover the mouse over a fish. What do you see? Does this seem to be related to theLocation toString
method?)- Look at the class documentation for the Grid Package
Direction
class. What methods does it have that returnDirection
objects?- Assume that
grid
is a valid 10 x 10BoundedGrid
object. Consider the following code segment.
Location loc1 = new Location(7, 3); Location loc2 = new Location(7, 4); Direction dir1 = grid.getDirection(loc1, loc2); Direction dir2 = dir1.toRight(90); Direction dir3 = dir2.reverse(); Location loc3 = grid.getNeighbor(loc1, dir3); Location loc4 = grid.getNeighbor(new Location(5, 2), dir1);
West East
- What locations would you expect in the
ArrayList
returned by a call togrid.neighborsOf(loc1)
after this code segment?- What should be the value of
dir1
?- What should be the value of
dir2
? ofdir3
?- What location should
loc3
refer to?- What location should
loc4
refer to?
Optional Programming Exercise: Location and Direction
- Write a simple driver program (similar to the one in
SimpleMBSDemo1
) that constructs aBoundedGrid
environment and then test your answers to the Analysis Questions above. TheArrayList
,Location
, andDirection
classes all implement thetoString
method to provide useful information, so you can useSystem.out.println
to see the values you are changing.- In your driver program, use the
inDegrees
method from theDirection
class to discover the degree representations for theDirection
constantsNORTH
,SOUTH
,EAST
, andWEST
. What is the value ofdir3
in degrees?
The Fish
Class
Next, Jamie turned to the Fish
class, whose code I needed to
understand well in order to modify it effectively.
A Fish
object has several attributes, or features. It has an
identifying number, its ID, which is useful for keeping track of the
different fish in the simulation, especially during debugging. It has a
color, which is used when displaying the environment. A fish also keeps
track of the environment in which it lives, its location in the
environment, and the direction it is facing. The Fish
class
encapsulates this basic information about a fish with its behavior. A
list of methods from the Fish
class appears below.
These methods are also documented on the MBS Quick Reference Sheet.
public Fish(Grid env, Location loc, Direction dir)
| Constructs a fish at the specified location and direction in a given environment. (one of 3 constructors) |
Accessor methods | |
public int id()
| Returns this fish's ID. |
public Color color()
| Returns this fish's color. |
public boolean isInAGrid()
| Checks whether this object is in a grid. |
public Grid grid()
| Returns the grid in which this grid object exists. |
public Location location()
| Returns this fish's location. |
public Direction direction()
| Returns this fish's direction. |
public String toString()
| Returns a string representing key information about this fish. |
Action and modifier methods | |
public void act()
| Acts for one step in the simulation. |
Protected helper methods used by public methods | |
protected void addToGrid(Grid grid, Location loc)
| Adds this object to the specified grid at the specified location. (used indirectly by constructor) |
protected void move()
| Moves this fish in its environment.
(used by act method)
|
protected Location nextLocation()
| Returns the row associated with this location.
(used by move method)
|
protected ArrayList
| Finds empty locations adjacent to this fish.
(used indirectly by move method)
|
protected void changeLocation(Location newLoc)
| Modifies this fish's location and notifies the grid.
(used by move method)
|
protected void changeDirection(Direction newDir)
| Modifies this fish's direction.
(used by move method)
|
One thing I noticed about the methods labeled "Helper Methods" in this list
was that they are declared protected, rather than public or private. I was
not familiar with the protected keyword. Jamie told me that it would be
useful when I started creating new types of fish, but that in the meantime
I could pretend that the helper methods are private. Like private methods,
they are internal methods provided to help methods in the Fish
class get their job done, and are not meant to be used by external client
code.
Constructors and Fields (Instance and Class Variables)
The Fish class actually has three constructors, even though only one is
shown in the list above. All three require that the client code
constructing the fish specify the environment (grid) in which the fish will
live and its initial location in that environment. The second constructor
allows the client code to specify the fish’s direction, while the third
allows the client code to also specify the color. Rather than repeating
the code to initialize a new fish’s state in all three constructors, Jamie
took advantage of the special this
constructor call, which
allows one constructor to call another specifying additional parameters.
Jamie only had to write all of the details for Fish
construction in the constructor with the most parameters, and then call
that constructor from the other two. Those constructors pass random
directions and colors for fish whose direction and color were not provided
by the client code. The BoundedGrid
class has a
randomDirection
method, which the first Fish
constructor uses to randomly choose a direction for the fish.
Unfortunately, the Color
class does not provide a
randomColor
method, but the Grid Package provides a
NamedColor
class which does have a getRandomColor
method. Both of the first two Fish
constructors call that
method to pass a random color to the third constructor.
The client code of a class is code outside of that class that uses its objects and methods. For example, if we have two classes, A and B, and there is code in class A that constructs objects of class B and invokes methods from class B on those objects, then class A is client code of class B. Of course, there might be another class, MainApp, that constructs one or more objects of class A and invokes A's methods; in that case, MainApp would be client code of class A. The code in class A is both client code of class B, and internal code for class A.
Analysis Questions: Client Code
(A Markdown template for writing up answers to
- Where have you seen client code of the
Fish
class? Where have you seen client code of theSimulation
class?- Which
Fish
constructor was called by client code you have seen? How did calling that constructor manage to initialize all of the attributes for the newly constructed fish?Fish
-related Analysis Questions is here.)
The third constructor — the one that takes four parameters to specify
the grid, fish location, direction, and colors — is responsible for
fully initializing the fish. It only actually initializes three instance
variables, though, which are all the instance variables defined in the
Fish
class. Even though a fish must keep track of its ID, its
environment, its location in the environment, its direction, and its color,
only the ID, direction, and color are stored in instance variables in the
Fish
class, and initialized directly in the Fish
constructor.
The key to understanding this is to recognize that the Fish
class is a subclass of the GridObject
class from the Grid
Package. A GridObject
is what the Grid Package expects to
store in a grid. After all, the package doesn't know
anything about the client code that will use it; will it be storing fish,
or color blocks, or physics particles, or marching band members?
All it knows is that all objects in a grid
need to know their own location in the grid, and they might need to act in
some way. The GridObject
class encapsulates these basic
needs. It has instance variables that know about the grid and the object's
location in the grid, and it provides methods to add an object to a grid,
report on an object's location in the grid, and change an object's location
in the grid. It even provides a placeholder act
method
(which doesn't actually do anything).
The Fish
class extends the GridObject
class,
including additional instance variables for the fish's ID, direction, and
color, and adding accessor methods and methods that allow the fish to move
when asked to act. The Fish
constructor uses the special
super
keyword to call the superclass
(GridObject
) constructor, which initializes the inherited
instance variables, then initializes the additional instance variables
that are specific to the Fish
class. To initialize the
myID
instance variable, it uses the Fish
class
variable, nextAvailableID
, which is a single integer that all
Fish
objects have access to. Each fish initializes its own ID
to the current value of nextAvailableID and then increments the class
variable for the next fish.
Since nextAvailableID
is a class variable and not tied to any
single object of the Fish
class, it must be initialized when
it is declared in the class rather than in the Fish
constructor.
Exercise and Analysis Questions: Instance and Class Variables
- Look over the code for the
Fish
class. How can you tell thatnextAvailableID
is a class variable?- Run the simulation program with either of the two demo programs. Hover the mouse over the fish to see their ID numbers? What are they? Is that what you expected?
- What would the ID numbers for the those fish be if the
nextAvailableID
class variable were an instance variable instead? Run the experiment to test your intuition and understanding of class variables.
Analysis Questions: Constructors
- Based on what you saw in the
fish.dat
data file, whichFish
constructor do you think is used by the graphical user interface when it reads an initial configuration from a file?- If you haven't already, run the Marine Biology Simulation program and construct and populate the environment manually, rather than from a file. Based on what you see when you manually place new fish in the grid, which
Fish
constructor do you think is used by this aspect of the graphical user interface?
Inheriting and Redefining Methods
The list of Fish
methods above included methods inherited from
GridObject
as well as methods defined in the Fish
class. The list below shows the same methods, but indicates which methods
are inherited from GridObject
(greyed out), which are new
in the Fish
class, and which were defined in the
GridObject
class but are redefined in the Fish
class (act
and toString
).
public Fish(Grid env, Location loc, Direction dir)
| Constructs a fish at the specified location and direction in a given environment. (one of 3 constructors) |
Accessor methods | |
public int id()
| Returns this fish's ID. |
public Color color()
| Returns this fish's color. |
public boolean isInAGrid()
| Checks whether this object is in a grid. (inherited from GridObject) |
public Grid grid()
| Returns the grid in which this grid object exists. (inherited from GridObject) |
public Location location()
| Returns this fish's location. (inherited from GridObject) |
public Direction direction()
| Returns this fish's direction. |
public String toString()
| Returns a string representing key information about
this fish.
(redefines behavior in
GridObject )
|
Action and modifier methods | |
public void act()
| Acts for one step in the simulation.
(redefines behavior in
GridObject )
|
Protected helper methods used by public methods | |
protected void addToGrid(Grid grid, Location loc)
| Adds this object to the specified grid at the specified location. (used indirectly by constructor) (inherited from GridObject) |
protected void move()
| Moves this fish in its environment.
(used by act method)
|
protected Location nextLocation()
| Returns the row associated with this location.
(used by move method)
|
protected ArrayList
| Finds empty locations adjacent to this fish.
(used indirectly by move method)
|
protected void changeLocation(Location newLoc)
| Modifies this fish's location and
notifies the grid. (used by move method)
(inherited from GridObject)
|
protected void changeDirection(Direction newDir)
| Modifies this fish's direction.
(used by move method)
|
Inheritance: A subclass inherits all of the data and behavior (instance variables and methods) of the class it extends (its superclass). It may also add new data and behavior. Finally, it may redefine some of the behavior of its superclass by providing new implementations of those methods in the subclass. (This is also known as overriding the superclass method.)
For example, theFish
class inherits a private instance variable calledmyLoc
from theGridObject
class, although it can only access it through the inheritedlocation()
method. Since the location is private, code in theFish
class can only set or change the location through the inheritedaddToGrid
andchangeLocation
methods. TheFish
class also redefines (overrides) theGridObject
toString
method to provide more fish-specific information, and redefines theact
method to move the fish.
At this point, Jamie told me about the difference between the
private
and protected
keywords.
Private
instance variables and methods can only be used within
the class in which they are defined. Protected
instance
variables and methods can be used in that class and also in any subclasses.
So, the private myLoc
instance variable can only be accessed
directly within the GridObject
class, but the protected
addToGrid
and changeLocation
methods can be
accessed by code in the Fish
class (or any other subclasses).
Exercise and Analysis Question: Inheriting and Redefining Methods
- Run the simulation program and hover the mouse over a fish. What do you see? How is this related to the
toString
method defined in theFish
class? Temporarily comment-out thetoString
method inFish
(don't delete it!), and re-compile and run the program. How (and why) has the behavior changed? (Then uncomment theFish
toString
method.)
Fish Movement
act
:
The act
method is the key to fish movement, or any other
behavior, since it is the only public non-accessor (or non-"observer")
method. The act
method does almost nothing, though. It
checks to make sure the fish is in a grid and, if it is, tells the fish to
move.
move
:
The protected move
helper method is the real heart of this
class. It first looks for a location to which the fish can move, using the
nextLocation
method. If the fish can’t move,
nextLocation
returns the current location. If the new location
is not the current location (so the fish can move), move
calls
another helper method, changeLocation
, to actually move there.
The fish also changes direction to reflect the direction it moved. For
example, if a fish facing north at location (4, 7) were to move west to
location (4, 6), it would change its direction to west. In
pseudo-code, we could write:
Move method: Get next location. (callnextLocation
) If next location not equal to current location, Move to the next location. (callchangeLocation
) Change direction if necessary. (callchangeDirection
)
The diagram to the right shows the relationship between these methods. The
act
calls the move
method, which calls the
nextLocation
, changeLocation
, and
changeDirection
methods. The diagram also shows that
nextLocation
returns a location to the move
method, which passes that down to the changeLocation
method.
The move
method passes a direction to
changeDirection
.
In the actual code, the move
method uses the
equals
method to compare locations rather than ==
or !=
operators because it only cares whether the row and
column values are the same, which is what the equals
method
checks, not whether the two locations are exactly the same
Location
object.
In addition to the code that implements the informal pseudo-code above,
the move
method and the methods it calls include several calls
to Debug.println
. The Debug class is a utility class in the
Grid Package. Debug.println
is like
System.out.println
, except that the string will only be
printed if debugging has been turned on. There are other methods in the
Debug
class to turn debugging on and off.
Aside:
I asked Jamie why the logic for fish movement is broken down into these
little subtasks (some of which are broken down still further), rather than
putting all of it right in the move
method. Jamie said that
this is an example of "best practices" in software development. (This one
is called the "template method pattern.") Breaking a task into smaller
tasks at different levels of abstraction helps the programmer to manage
complexity, makes testing easier, and, as I discovered later in my
internship, helps to reduce duplication of code across related classes.
Exercises and Analysis Questions: Move Method Details
- Can a fish (or other grid object) exist outside of a grid? Look at the methods in the
GridObject
class to substantiate your answer.- Consider the following variable definitions.
Location loc1 = new Location(7, 3); Location loc2 = new Location(2, 6); Location loc3 = new Location(7, 3);
- What does the expression
loc1 == loc1
evaluate to? What aboutloc1.equals(loc1)
?- What does the expression
loc1 == loc2
evaluate to? What aboutloc1.equals(loc2)
?- What does the expression
loc1 == loc3
evaluate to? What aboutloc1.equals(loc3)
?Edit either
SimpleMBSDemo1
orSimpleMBSDemo2
and add the lineedu.kzoo.util.Debug.turnOn();somewhere before your code that causes fish to move. Run the program to see the effects of theDebug.println
statements. (If youimport edu.kzoo.util.Debug
at the top of your demo driver, you can turn on debugging with justDebug.turnOn()
.)(You may then want to comment out the
Debug.turnOn()
statement or callDebug.restoreState()
. Debugging messages can be useful to illuminate what is happening in small sections of code, but they can be overwhelming if they come from every part of a program.)
nextLocation
:
The nextLocation
method finds the next location for the fish.
The fish the biologists were simulating are equally likely to swim forward
or to either side, but virtually never turn completely around to swim
backwards. In the simulation, therefore, a fish may move to the cell
immediately in front of it or to either side of it, so long as the cell it
is moving to is valid (in the environment) and empty. It never moves to the
cell behind it.
The first thing nextLocation
does, therefore, is to get
a list of all the neighboring locations that are in the environment and
that are empty. It calls the emptyNeighbors
helper method to
do this. Then it removes the location behind the fish from the list.
Finally, nextLocation
randomly chooses one of the valid empty
locations and returns it (or returns the current location, if the fish
can’t move). The pseudo-code below summarizes the activities of the
nextLocation method.
Next Location method:
Get list of neighboring empty locations. (call emptyNeighbors
)
Remove the location behind the fish.
If there are any empty neighboring locations,
Return a randomly chosen one.
Otherwise,
Return the current location.
emptyNeighbors
:
In emptyNeighbors
, the fish first asks the grid for a list of
all its neighboring locations. Since the neighboring locations are not
necessarily empty, emptyNeighbors
constructs a new
ArrayList
and copies all the neighboring locations that happen
to be empty into that list. When it's done, it returns the new list of
empty neighboring locations.
The diagram to the right shows this relationship between the
neighborsOf
method in the grid, which return a list of all of
the neighboring locations (empty or not) to emptyNeighbors
,
which in turn returns a list of only empty neighbors to
nextLocation
, which chooses one empty neighbor to be the next
location.
Analysis Questions: Next Location and Empty Neighbors
- Why does the
Fish
class need anemptyNeighbors
method? Why doesn’tnextLocation
just call theneighborsOf
method from theBoundedGrid
class?- How many neighboring locations would you expect there to be in the list generated by
emptyNeighbors
? Does it always return the same size list?- Consider the following bounded grids containing fish.
- How many neighboring locations would the
BoundedGrid
neighborsOf
method return for location (1, 0) in each grid?- How many neighboring locations would
emptyNeighbors
return for the fish in location (1, 0) in each grid?- Are the neighboring locations returned by
emptyNeighbors
always going to be valid locations? In other words, will they always be locations in the grid, or might it include some invalid locations (outside the bounds of the grid)?- After
emptyNeighbors
returns tonextLocation
, how does thenextLocation
method remove the location behind the fish from the list of locations it might move to?Note: There are tworemove
methods inArrayList
. One takes an integer index as a parameter and removes whatever object is at that index; the other takes an object as its parameter and finds and removes the equivalent object from the list.- The Grid Package provides a
RandNumGenerator
class that is similar to (it actually extends) thejava.util.Random
class that you have used in previous projects. The difference is that it provides a getInstance method that always returns the same, single random generator object. Anywhere in your code, you can call RandNumGenerator.getInstance() to get a reference to the same generator object, rather than constructing new ones every time you need one. What is the advantage of this? (If you're not sure, you may want to read the class documentation for theRandNumGenerator
class, which is in theedu.kzoo.util
package.)
changeLocation
:
Once the move
method knows where to move (as determined by
the nextLocation
method), it calls the
changeLocation
method it inherits from GridObject
to actually move there. The changeLocation
method notifies
the grid of the change, since the grid keeps track of the locations of all
its objects. It is critical that the fish and the grid agree where the
fish is. (This is true for any object in the grid.)
changeDirection
:
If the fish had to turn to move (it did not move forward),
move
calls the changeDirection
method to update
the fish's direction. Unlike changeLocation
, the
changeDirection
method is not inherited from
GridObject
. The Grid Package does not assume that objects
in the grid keep track of a direction.
Overall Notes on the Fish Class
Note that many of the methods in the Fish
class use the
class's accessor methods, such as grid()
,
location()
, and direction()
, rather than
accessing instance variables directly. This is good programming
practice, even though it’s a little harder to read, because it limits the
number of places in the code that depend on the internal representation of
the object’s data. (It also turned out to be useful later when implementing
other types of fish.) Obviously, the accessor methods themselves, and the
methods that changed instance variable values, have to access the instance
variables directly.
Analysis Questions: Exploring Inheritance
- Look at the class documentation for the MBS
Fish
class and theColorBlock
, andTextCell
classes in the Grid Package.Remember that the full set of methods for any class is the combination of all inherited and new methods. Also, notice that many method names within the Grid Package are clickable, so you can follow a link to get more information on inherited methods.
- How can you tell whether they extend another class?
- How deep is the inheritance "family tree" for these classes?
- How can you tell what methods they inherit and what methods they define?
- The
Location
class is obviously not a subclass ofGridObject
(one doesn't placeLocation
objects into the grid). Does it extend some other class? What is its complete list of methods?- Not all of the methods listed above for the
BoundedGrid
class are actually defined in that class. How many are? What is the inheritance "family tree" forBoundedGrid
? Identify which methods come from where.- Look at the class documentation for the
MBSGUI
class in thembsgui
folder. How deep is its inheritance "family tree"?
- Look at the methods inherited from the class right above it in the class hierarchy, and at the methods inherited from the class at the top of the hierarchy. Do any of them look familiar?
- The class has almost no methods in it. (You can see this in the class documentation, but you can look at the code, too, if you want.) Where are the methods that would correspond to the
step
andrun
functions you saw in theSimulation
class?