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


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.

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).

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


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


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.