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 program’s 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) once—before the simulation actually begins—and 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 there’s 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, let’s examine how the Environment object is treated in the program’s main function (in fishsim.cpp), an abbreviated fragment of which is given below:

Environment env(input);
Display display;
Simulation sim;

..
for (step = 0; step < numSteps; step++) {

   
sim.Step(env);
    display.Show(env);
   ..
}    
        
[excerpted main from fishsim.cpp]                                 

From the fragment we can see the program’s 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. Next—and omitted from the fragment—come statements which perform the housekeeping for entry to the for loop.

For each iteration of the for loop, the Simulation object’s Step function triggers the movement of every fish represented in the Environment object env. Then the Display object’s Show function prints the new state of env.  Notice that the Environment object doesn’t 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 object’s Step function and the Display object’s 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 program’s 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) {
// postcondition: one step of simulation in env has been made
  apvector<Fish> fishList;
  int k;
  fishList = env.AllFish();
  for (k = 0; k < fishList.length(); k++)
  {
     fishList[k].Move(env);
  }
}                                      [from simulate.cpp
]

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 it’s true that the Fish object’s 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 object’s Step function, the Fish object’s Move function is actually responsible for modifying the Environment object. In light of this fact:
    a. Should the Step function’s reference parameter be changed to a const reference?
    b. What would happen if the Step function’s 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 it’s 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 program’s 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 hiding—the 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 program—informs 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, let’s 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 object’s 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 object’s AllFish function returns an apvector of Fish objects in the  prescribed order. The details of the  Environment object’s implementation are irrelevant to the Step function.

Why is this advantageous?  Suppose that the programmers determined that changing the Environment object’s 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 object’s 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