Gary Jaye's
Guide to the Marine Biology Case Study (1)
MCX1 home MCX2 home
CONTENTS
1. Overview of Part II
2. The Program infrastructure: the
Position and Neighborhood classes
3. The Environment::IsEmpty function and
Alyce Brady's object diagram
3a. Introduction to
the Environment class
3b. Programming:
modifying the Environment class
4. The Fish::EmptyNeighbors function
and Brady's object diagram
4a. Introduction to
the Fish class
4b. Programming: modifying
the Fish::ShowMe
5. The Fish::Move function and Brady's object
diagram
5a. Programming:
mini-project
6. The Environment::Update function
and Brady's object diagram (under construction)
7. Projects (under
construction)
8. Answers to questions
1. Overview of Part II:
1. The Environment class: The
programs single Environment object stores and manages the data of the simulated
environment that it represents using these data members:
| private: .. apmatrix<Fish> myWorld; int myFishCreated; int myFishCount; .. [from environ.h] |
The apmatrix myWorld provides a 2-dimensional grid of positions that serves to map the environment. For example, myWorld[r ][c] represents the state of the position at coordinates (r, c) in San Francisco Bay. In addition to providing a mapping scheme to represent the environment, myWorld also stores the Fish objects through which the simulation is implemented. As the program is written, the members myFishCreated and myFishCount store the same values since all the Fish objects in a simulation are instantiated (created) oncebefore the simulation actually beginsand no Fish objects are destroyed during a simulation.
2. The Fish class: Every fish is represented by a Fish object defined with these three data members:
| private: .. int myID; position myPos; bool amIdefined; .. [from fish.h] |
The identifiers myID and myPos
suggest the roles of the members to which
they refer. When a Fish object is created it receives a unique identification number that is
stored to myID. The current position (in the Environment object's myWorld member)
of every simulated fish is stored in myPos.
The amIdefined data member indicates whether or not its owner currently represents
a fish. Why a member with this role should be necessary
is a question best answered by considering the Environment object in which individual Fish
objects are stored. Clearly theres an
advantage to a design that stores a fish in the (subscripted) position to which it is
being mapped in the simulated environment. This design, however, has shortcomings as well
as advantages. One shortcoming arises from the fact that if myWorld[r][c] is not
occupied by a simulated fish, it nevertheless refers to a Fish object! To solve the
problem of determining whether or not a Fish object in myWorld actually represents
a fish, the authors of the program included the boolean member amIdefined in their
definition of a Fish object.
3. The Simulation and Display classes: The program implements the marine biology simulation by modifying its single Environment object in a manner according to these criteria.
1. The order in which a fish is moved is determined by the subscripted position of the Fish object that represents it in myWorld:
fish in lower rows are moved before higher ones, and--within rows--fish in lower columns move before those in higher ones.
2. Relative to its own position, a fish can move one coordinate unit up, down, left, and right. If any of these four positions are
occupied or out-of-bounds, the possibility of moving to it is eliminated.
Putting aside the question of how the movement of each individual fish is implemented until later, lets examine how the Environment object is treated in the programs main function (in fishsim.cpp), an abbreviated fragment of which is given below:
Environment
env(input); |
From the fragment we can see the programs strategy for achieving a simulation: First, objects of the Environment, Display, and Simulation classes are instantiated (created). Clearly there is no need to instantiate more than one object of any of these three classes. Nextand omitted from the fragmentcome statements which perform the housekeeping for entry to the for loop.
For each iteration of the for loop, the Simulation objects Step function triggers the movement of every fish represented in the Environment object env. Then the Display objects Show function prints the new state of env. Notice that the Environment object doesnt manage a simulation step or the display task itself: the Simulation and Display objects, respectively, handle these tasks. This leads to our first question:
Question 1.1: The previous
fragment tells us that an Environment object is a parameter of the Simulation
objects Step function and the Display objects Show function. If
you were designing the program
a. In what respect would you define
the parameters in both functions the same way?
b. In what respect would you define
the parameters differently?
4. The Simulation class Step function: The Step function gives us an opportunity to see how the programs Simulation object interacts with the Environment object and how the Environment object interacts with the Fish objects which are the elements of its apmatrix myWorld.
void
Simulation::Step(Environment & env) { |
The code is fairly straightforward. First, the Environment object transmits an apvector of Fish objects through its AllFish function. As a result, the local object fishList contains a copy of every fish represented in the simulation. Can you state the order in which AllFish copies Fish objects from myWorld to build the apvector it returns? See The order in which a fish is moved if you can't. Next, the for loop enables the Step function to traverse the fishList object, thereby visiting each Fish object in the specified order. Notice how the Step function moves each successive fish:
fishList[k].Move(env);
Since the expression fishList[k] refers to a Fish object, the expression fishList[k].Move(env) implies that Move must be a member function of the Fish class. Therefore, we can conclude that each Fish object actually moves itself! While its true that the Fish objects Move function is invoked by the Step function, it is clear that the actual moving is performed by the Fish object.
Question 1.2: We have seen that
in the Simulation objects Step function, the Fish objects Move
function is actually responsible for modifying the Environment object. In light of this
fact:
a. Should the Step
functions reference parameter be changed to a const reference?
b. What would happen if the Step
functions reference parameter were changed to a const reference?
5. Information hiding in the case study program's design: The case study program provides the student with the opportunity to study and work with code of moderate complexity. While its true that much of the complexity has less to do with the marine biology simulation problem than with the deliberately elliptical and overly elaborate fashion in which the programs authors implemented their solution, the complexity of the resulting code is not unrealistic. Entropy characterizes all real world systems, particularly those that evolve over time to address problems or conditions that they were not originally designed to meet.
Information hidingthe programming technique in which the details of a program unit such as a function or the private members of a class are made invisible (inaccessible) to the rest of the programinforms the design of the case study. Not an end in itself, information hiding provides a means to achieving generality and functional modularity, two qualities that make a program easier to organize, develop, and maintain. To see how information hiding promotes generality and modularity, lets re-examine the main and the Simulation::Step functions.
| Environment
env(input); Display display; Simulation sim; .. for (step = 0; step < numSteps; step++) { sim.Step(env); display.Show(env); .. } [excerpted main from fishsim.cpp] |
void
Simulation::Step(Environment & env) { apvector<Fish> fishList; int k; fishList = env.AllFish(); for (k = 0; k < fishList.length(); k++) { fishList[k].Move(env); } } [from simulate.cpp] |
Consider what the Simulation::Step function needs to know about the Environment
object with which it interacts. Despite the fact that the Environment objects use of
an apmatrix of Fish objects to represent the environment is transparent to the Step
function, the function is able to perform its task. The Step function only needs to
know that the Environment objects AllFish function returns an apvector
of Fish objects in the prescribed order. The details of the Environment
objects implementation are irrelevant to the Step function.
Why is this advantageous? Suppose that the programmers determined that changing the Environment objects implementation from the current strategy of using an apmatrix of Fish objects to a strategy that employs an apmatrix of bools and an apvector of Fish objects would be beneficial or necessary: As long as the Environment objects AllFish function continued to accomplish its task, the implementations of the Simulation::Step and the Display::Show functions would not be affected. Without information hiding, the flexibility and generality gained therein would not only be difficult to achieve, but would be almost impossible to enforce as the program evolved over time.
2. The program infrastructure: the Position and Neighborhood classes
The case study program uses a Position object to represent the coordinates of a fish and a Neighborhood object to store a collection of Position objects. Since these two simple classes play supporting roles in the program, mastering them is a prerequisite to mastering the case study program. And since the implementation of these classes is fairly simple and direct, the questions in this section are designed to review essential C++ class syntax and to encourage a healthy class consciousness.
1. The Position class. In
the header file in which the Position class is defined, position.h,
a description of the class and instructions on how to interact with its objects are given:
A Position represents a (row,column) in a grid whose (0,0) is
upper-left as in matrix coordinates. Once constructed, a position doesn't change (all
member functions are const), although a Position can be assigned to an existing Position.
For example:
Position p(2,3);
Position q; // default (-1,-1)
q = p; // q now at (2,3)
Adjacent Positions of a given Position can be determined as illustrated:
Position p(5,5);
Position q;
q = p.North(); /* q is (4,5)
*/ q = p.South(); // q is (6,5)
q = p.East(); /*
q is (5,6) */ q = p.West(); //
q is (5,4)
This is an an excerpted definition, from position.h, of the Position class and its associated free functions:
class Position {
public:
// constructors
Position(); // postcondition: Row() == -1, Col() == -1
Position(int r, int c); // postcondition: Row() == r, Col() == c
// accessing functions
int Row() const;
int Col() const;
Position North() const;
Position South() const;
Position East() const;
Position West() const;
bool Equals(const Position & rhs) const;
apstring ToString() const;
private:
int myRow;
int myCol;
};
// free functions
ostream & operator << (ostream & out, const Position & pos);
// postcondition: pos inserted onto stream out
bool operator == (const Position & lhs, const Position & rhs);
// postcondition: returns true iff lhs == rhs
Question 2.1: On the basis of
the definition above, test your class consciousness:
a. What is the purpose of the const
qualifier in the declarations of the accessing functions?
b. Why isn't the the const qualifier used in a similar
manner to define the operator== free function?
c. Which statement declares the
copy constructor? The default constructor? The explicit-value constructor?
Question 2.2: Complete the
implementation of the Row function started below.
int
Position::Row(void) const {
}
Question 2.3: The authors of the
program implement the operator== function by
using the Equals Position member function. Complete the implementation started
below without using the Equals function.
bool operator == (const Position
& lhs, const Position & rhs) {
}
2. The Neighborhood class. In the header file in which the Neighborhood class is
defined, nbrhood.h,
a description of the class and instructions on how to interact with its objects are given:
Class Neighborhood represents a collection of
Positions. Positions can be added to a Neighborhood. Each Position in a Neighborhood is
accessible via the functions Select() -- choose a Position --and Size() -- return the # of
Positions in a neighborhood. In the current implementation, a maximum of 4 Positions can
be added to a neighborhood. Any call of Add() after the fourth call is ignored.
This is an an excerpted definition, from nbrhood.h,
of the Neighborhood class and its associated free function:
class Neighborhood {
public:
// constructor
Neighborhood(); //postcondition: Size() == 0
// accessing functions
int Size() const; //postcondition: returns # Positions in the neighborhood
Position Select(int index) const; //access a Position
//precondition: 0 <= index < Size()
//postcondition: returns the index-the Position in Neighborhood
apstring ToString() const;
//postcondition: returns a string version of all Positions in Neighborhood
// modifying functions
void Add(const Position & pos); // add pos to neighborhood
//precondition: there is room in the neighborhood
//postcondition: pos added to Neighborhood
private:
apvector<Position> myList;
int myCount;
};
ostream & operator << (ostream & out, const Neighborhood & nbrhood); //postcondition: nbrhood inserted onto stream out
Question 2.4: By looking at the
Neighborhood class definition above, you should be able to answer these questions:
a. How does a Neighborhood object determine how many Position values
have been stored to myList?
b. How does a free function determine how many Position values have
been stored to a Neighborhood object?
c. Which member function enforces the specification that a Neighborhood
object can store at most four Position values?
Question 2.5: Complete the implementation of the Add function started below.
void Neighborhood::Add(const
Position & pos) {
}
Question 2.6: The MBCS authors'
implementation of the Add function (see Answers
or nbrhood.cpp) checks if its maximum storage count
has been reached with the condition (myCount < myList.length()) rather than (myCount < 4).
a. Which member function sets myList's size to four?
b. Is the authors' condition (myCount < myList.length()) preferable to the more straightforward
condition (myCount
< 4)?
Question 2.7: Revise the implementation of the default constructor (of the Neighborhood class) given below by using a constructor initializer list.
Neighborhood::Neighborhood() {
myCount = 0;
myList.resize(4);
}
}
3. The Environment::IsEmpty function and Brady's object diagram
In the case study program objects of a class interact with or are constituted by objects of other classes. In the previous section, for instance, we saw how a Neighborhood object was implemented as an apvector of four Position objects. In the first section we observed that using an apmatrix of Fish objects was the critical strategy for implementing the program's Environment object. To represent the interaction between objects of different classes that characterizes the design of the case study program, Alyce Brady has devised what she calls object diagrams. Consider the Brady object diagram representing the Environment::IsEmpty function:
Before reviewing the implementation,
consider how the interaction defined by the Environment::IsEmpty function is
depicted in the object diagram. Notice that (1) data members are not represented in the
diagram and (2) member functions are depicted two different ways.
Now consider the object diagram
in relation to the implementation (in environ.cpp) of
the IsEmpty function that it represents:
bool Environment::IsEmpty(const Position & pos) const {
// postcondition: returns true if pos in grid and no fish at pos,
// returns false otherwise
if (! InRange(pos)) {
return false;
}
if (myWorld[pos.Row()][pos.Col()].IsUndefined())
{
return true; // pos in grid and no fish at pos
}
return false;
}
The
object diagram clearly indicates that IsEmpty calls the Fish::IsUndefined,
Position::Row, Position::Col, and Environment::InRange
functions. The object diagram also indicates that, in addition to the IsEmpty
function, the Environment::InRange function also invokes the
Position::Row and Position::Col functions. Notice, however, that the
implementation code does not and cannot reflect this last fact concerning
the Environment::InRange function without compromising the program design
principle of information hiding
discussed in Section 1.
Consequently, it is fair to say that Brady's object diagrams represent
aspects of a member function's underlying activity that its implementation code may
not be able to reflect. Conversely, while Brady's object diagrams reflect the relations
between objects of different classes, they do not and cannot represent the algorithms or
the code with which a member function is implemented. But they provide an extremely useful
and complementary mechanism for examining the key member function in the Marine
Biology case study.
Question 3.2: By now you should
have a good idea about what each of the eight Environment class member functions do.
a. What is
the return type of AllFish and where have we seen it called?
b. What do you suppose InRange does?
3a. Introduction to the Environment class
Using
the depiction of the Environment object in the object diagram above, you should be able to
formulate reasonable conclusions concerning the implementation and properties of the
Environment class.
Question 3.3: Consider
member functions NumRows and NumCols.
a. What do you suppose they do?
b. Write the prototype for NumRows.
c. Implement NumRows in the space provided below.
Question 3.4:
Since it's known that Fish objects are neither created nor destroyed after the Environment
constructor instantiates an Environment object, what is the only Environment member
function (as the program is currently written) that could possibly call Environment::AddFish?
Now examine how the Environment constructor is implemented (in environ.cpp):
Environment::Environment(istream & input) {
: myWorld(0,0),
myFishCreated(0),
myFishCount(0)
// precondition: input is open for reading, in correct format
// postcondition: environment initialized and populated from input
int numRows, numCols, row, col;
if (input >> numRows >> numCols) { // resize the matrix
myWorld.resize(numRows, numCols);
}
else {
cerr << "reading rows/columns failed in Environment" << endl;
exit(1);
}
while (input >> row >> col)
{
AddFish(Position(row, col));
}
}
Notice the use of a constructor initializer list to initialize all three data members of an Environment object:
| private: .. apmatrix<Fish> myWorld; int myFishCreated; int myFishCount; .. [from environ.h] |
Question 3.5:
Which member function is responsible for changing the values of data members myFishCreated
and myFishCount?
Question 3.6:
Throughout each simulation run, what is the relation between the values stored to myFishCreated
and myFishCount?
Question 3.7
DESIGN QUESTION: Since the Environment
constructor is the only function or member function in the case study program that calls
the Environment::AddFish function, why did the program's authors qualify
its access level as public when private would have
sufficed? What purpose do you think this serves?
3b. Programming: modifying the Environment class
To
prepare for this exercise, be sure to have hard copies of environ.h and environ.cpp on
hand. You will also want to create a working directory containing all the files in 91-APmarine1
(which contains all the Marine Biology case study files in their original state) to ensure
that you always have copies of the original files on hand.
part 1. Revise function Update to remove any fish
scheduled to move to a position whose row value equals its column value with the help of
new a public member function, Environment::RemoveFish, by
following these instructions:
(a) Add the declaration void RemoveFish(const Position & pos); to
the Environment class declaration in environ.h.
(b) Implement RemoveFish in environ.cpp. using
the prototype given below.
void Environment::RemoveFish(const Position &
pos);
// precondition: a fish is at pos, i.e., ! IsEmpty(pos)
// postcondition: IsEmpty(pos), myFishCount
is decremented
c) Now use RemoveFish to
implement the specified revision in Update. Test your code by entering 999 when
the program prompts you
for the
number of steps to run. This should be more than enough to retire every fish from the
simulation.
part 2. Modify fishsim.cpp to
eliminate the prompt for the number of steps, set numSteps to 999, and announce
the number of steps.
part 3. Since the number of steps required to eliminate the
simulated population is usually far less than 999, it would be useful to find a way to
terminate the program when the population level hits zero. Modify AllFish to
terminate the program when then the population level hits zero. You will need to:
a. include stdlib.h in your working copy of environ.cpp
b. Use the standard library function exit: exit(0) terminates a program, even when called outside the main
function.
part 4. Define a destructor for Environment class by doing the
following:
a. In your working copy of environ.h, insert this
prototype below that of the constructor: ~Environment();
b. In your working copy of environ.cpp, complete the
definition of a destructor that produces this output:
Number of fish in
simulation: 12
Number of surviving fish: 0
Number of fatalities: 12
c. Read this before testing your
destructor: Your destructor probably won't produce output when you first
test it. This doesn't mean
that it's been written incorrectly!
Assuming that the destructor has been correctly implemented, the absence of output
probably arises
from the fact that the exit
function terminates the program before your destructor has the opportunity to execute.
To force the destructor to execute from the AllFish
function, recall that (1) an object must go out of scope to trigger its destructor
and (2) the Environment object must go
out of scope before the exit function is called. One
solution to this problem is so ugly and
impractical that it may well need to be
spelled out. (Keep in mind that your objective here is to test the implementation of your
destructor,
even at the expense of practical programming.)
If you need a hint, see Programming hint.
part 5. In part 1, you added a fish fatality
mechanism to the Update function. Now you will be asked to build upon that
solution in this manner: After every third fish is removed from the simulation, one new
fish is to be added (born).
a. Use AddFish and consider adding a myFishDied
data member to the Environment class. Be certain that you don't attempt to add a fish
to
an occupied position.
b. Add one more output line to your destructor which
indicates the name of the last fish that was created. If your simulation concludes before
999 simulation steps have been executed,
your final line of output should be: The last fish born was:
Q
Examine the ShowMe member
function prototyped in fish.h
if you're not sure how to achieve this output.
Back to contents To next section MCX1 home MCX2 home