Lab:
Fish With Class
Implementing Classes
This set of Lab Exercises is the fifth in a series in which students
build a small program with several fish moving around in an aquarium.
The set includes the following exercises:
Each section contains an Introduction to a problem
or task
and an Exercise.
In the exercises that precede this one, students will have created a
list of fish that move randomly back and forth in an aquarium,
being careful not to hit the sides.
Students should understand how to
identify the responsibilities of different classes,
construct class methods,
and use instance variables within methods.
Responsible Fish
Introduction
One of the most important tasks in designing and implementing
object-oriented programs is deciding which classes or objects are
responsible for executing which behavior.
Up until now, all of the
behavior that you have added to the Aquarium Simulation program has been
in the main
method of the AquaSimApplication
class.
By now, however, the main
method
is starting to get long and tedious to work with.
It makes sense that this method wants to tell all of the fish to
move, but why is it concerned with the details of how a fish
moves, such as when it should reverse direction? Why isn't the
Fish
class responsible for knowing how a fish moves?
It's time to make the program more object-oriented.
In this exercise, you will implement a move
method in the
AquaFish
class that will encapsulate the
behavior related to movement, including changing direction and
moving forward.
Concept: Refactoring
As a program evolves, it is not uncommon to decide that a different
way of organizing the classes or methods in the program would make it
easier to read, to understand, or to enhance. It is not a good idea
to change the structure of existing code at the same time as adding
new functionality, because if the program stops working correctly it
will be difficult to know whether it is the restructured functionality
or the new functionality that is the cause of the problem. It is
better to rearrange or redesign the existing code first, test that
it still works correctly, and only then add any new functionality.
The restructuring or redesigning phase, without changing the program's
behavior, is called refactoring.
Exercise: Make Fish Responsible for Knowing How to Move
- Look through the
AquaFish class. Just
before the moveForward and
changeDir methods, there is a commented-out
skeleton
move method. Uncomment the move
method.
- Analyze the
main method of the AquaSimApplication
class to determine which lines of code should be moved to the move
method
in the AquaFish class.
- Move the appropriate lines of code from the
main method
in AquaSimApplication to the move in AquaFish .
(Stop and Think:
Everytime the main method was
invoking AquaFish
methods, such as
moveForward and
changeDir , it was doing so on a specific fish.
Once that code is moved to the move
method, what is the specific fish on which those methods
should be invoked? How should you invoke the methods?
- Modify the
main method in AquaSimApplication
to simply tell each fish to move, letting the fish worry about how
it should move.
- Now that
moveForward , atWall , and
changeDir are only used internally in the
AquaFish class, change the two modifying
methods (moveForward and
changeDir ) to be protected
rather than public . (There are
commented-out lines that do this already there. You
could uncomment them and delete the other lines that were
being used instead, or you could change the
public keyword to protected
in the current lines, using the commented-out lines as a
template. (The exact meanings of the
public and protected keywords
will be the focus of a future class.)
- Test your program in the same ways that you have tested it
before and make sure that the program behavior is consistent
with what it was before you refactored it (regression testing).
|
Concept: Color Representation
Colors are commonly represented as a mixture of a certain
amount of red, a certain amount of green, and
a certain amount of blue (this color representation is called
RGB, for red, green, and blue).
Each individual color amount is represented by a number between 0 and
255: black is (0, 0, 0) because it has no red, green, or blue; white is
(255, 255, 255) because it is made up of the maximum amount of red,
green, and blue; pure red is represented as (255, 0, 0); while orange,
which is made up of lots of red, quite a bit of green, and no blue, is
represented as (255, 200, 0). New colors can be constructed in Java by
passing the appropriate amounts of red, green, and blue to the
Color
constructor, as in the example below:
Color myColor = new Color(255, 0, 255);
Stop and Think
- Look through the
AquaFish class and see how
the fish's color is represented as part of its state. See
where it is set. How does the
AquaSimGUI class access a fish's color?
- Why does the
AquaFish class have two constructors?
What does each one do? In particular, what does the call to
this(...)
in the first constructor do?
Exercise: Make Fish Responsible for Their Color
- In the one-parameter
AquaFish constructor,
create randomly colored fish
rather than white fish. You will have to do this by
creating a Color object with random numbers as
the parameter to this, without using any local variables.
(This is because when one constructor calls another
constructor in the class or its superclass, that call most
be the first statement in the constructor.)
In other words, replace the Color.WHITE parameter
with a
new Color(...) expression, where each parameter
to the Color constructor is a call to the
random number generator's nextInt method to get
a random amount of red, green, and blue.
(Stop and Think: how many
numbers are there between 0 and 255? Hint: the answer
is not 255. How should you use this information in
deciding how to get your three random numbers?)
- Modify the
main method to construct fish
with randomly-generated colors rather than pre-defined colors.
- Test your modifications.
|
Modeling a Simulation
Introduction
In a well-designed object-oriented program, we usually want the
main
function to just create some objects and get the ball
rolling.
Most of the program behavior should be the result of the objects
interacting with each other.
In the Aquarium Simulation program, though, the main
function is actually running the simulation.
We have objects that model the fish and the aquarium,
but not one that models the simulation itself.
In this exercise, you will write the code for methods in a
Simulation
class. The constructor will initialize the
Simulation
object's instance variables and construct the
fish in the aquarium.
The step
method will
execute the commands that should happen each timestep in the simulation
(moving the fish, in our case).
Once you have implemented the Simulation
class, the main
method in AquaSimApplication
will merely create a number of objects,
such as the aquarium, the graphical user interface, and the simulation object,
and then ask the simulation object to run the simulation. The main
method will also still display the initial configuration of the fish and the
modified aquarium after each timestep.
Exercise: Introduce the Simulation Class
- Create a new
Simulation class in BlueJ.
When you are done, an object of this class will run the
aquarium simulation.
You can delete all of the internals of the class that were
provided when you created the class (between
the opening and closing braces), since the template class
and the class you are creating have very little in common.
- The new Simulation class will need a constructor and two
methods:
step and run . For now,
create a public, zero-parameter constructor with an empty body and
two public, zero-parameter, empty methods that return
nothing.
- The main purpose of a constructor is to initialize any
state (instance variables) instances of the class may have.
Your class doesn't have a state yet; you can create instance
variables and initialize them in the constructor as you find
you need them. So for now, concentrate on the two methods.
- Start with the
step method, since its behavior is
perhaps the clearest. Find the code in the
main method that
is executed in each step of the simulation. Move it to your
new step method. Notice that your code will not compile
because there are two objects that it needs that
it does not have access to: the list of fish and the user
interface. For now, pass those two items in as parameters.
Don't forget to import the ArrayList class at
the top of your class.
(Stop and Think: Your code may
have a third undeclared value representing the number of
fish. Why can you make do with just the two
parameters suggested here, rather than also passing in
the number of fish as a third parameter? )
- Construct a simulation object somewhere in your
main method
(Stop and Think: where would be a
good place in the main method to construct
it?)
and then call the step method where your code
for executing
a single step used to be.
Test your program.
- Next implement the
run method by moving the
code that runs the simulation from the main method.
In the main method, replace the moved code
with a call to the run method.
(Stop and Think: What objects
need to be passed as parameters to the run
method? How should the call to the step
method change?)
- Test your program to verify that the behavior is unchanged.
Stop and Think
Every time the main method calls the
run method, it passes the
list of fish and the user interface. Every time the
run method calls the step method, it
passes the same objects. Are those objects changing over time?
Are the values different every time they are passed to the
methods, or could the objects be passed to the simulation object
once when it is constructed and be part of the state of the
simulation? In fact, could one or both of them
just be part of the state of the
Simulation object, constructed in the
Simulation
constructor rather than passed to it as a parameter, and not
exist in the main method at all? Are there other
objects that you think should be part of the
Simulation state
rather than in the main method? If the job of the
main method is just to create a small number of objects
and then invoke a small number of methods to get the program
running, which objects should be created there and which should
be created by the Simulation object?
- Based on your analysis above, move the construction of
some of your objects from the main method to the Simulation
class, creating instance variables as necessary. Don't forget
to make them
private . Change the
parameters to your run and step
methods so that you are not passing the object's state around
as parameters to itself.
- Test your program to verify that the behavior is unchanged.
|
Ascending and Descending Fish
Introduction
Our program would be much more interesting if the fish moved up and down
as well as side to side.
In this exercise, you will implement two new methods in the
AquaFish
class, ascend
and descend
,
to support this behavior.
Exercise: Simulation Up and Down Movement
- To make the simulation more believable, the distance that a fish moves
up or down should be related to its height. Fish come in different sizes,
so the size of any particular fish is one of the properties of that
fish. Its position in the aquarium is another relevant property for
this exercise. Read the implementation
(code) for the
AquaFish class to determine which methods or instance
variables will be useful in implementing ascend and descend .
Also look at the class documentation for the
NavigationalAide
class to see how to make a fish rise and sink.
- Determine what parameters (if any) you will need for the new
ascend
method. Then determine what its return type should be. Add an empty
ascend method (one that consists of a declaration and empty
braces) to the AquaFish class after the moveForward
and changeDir methods.
- Implement the
ascend method. You may use the moveForward
method as a guide if you like, but the ascend method is
simpler. The movement amount should simply be the height of the fish.
- Implement the
descend method.
- Modify your
move method in the AquaFish
class to allow fish to ascend or descend before moving forward, according
to the following formula:
- A fish at the surface has a 2/3 chance of descending and a 1/3
chance of staying at the surface.
- A fish at the bottom has a 1/3 chance of ascending and a 2/3 chance
of staying at the bottom.
- A fish that is neither at the surface nor at the bottom has a
1/3 chance of ascending, a 1/3 chance of descending, and a 1/3 chance
of staying at the same depth.
- Test your program.
|
When you are finished modifying
AquaSimApplication
, AquaFish
, and
Simulation
, submit your work.
Copyright Alyce Faulstich Brady, 2001-2002, 2008-2009.