Chapter 7:


C201 Home Page


Chapter 7
Designing Classes

powered by FreeFind

Modified: 

 

 

 

 

Overview

Chapter 6 covered important ideas about programming: testing small units, automating test, regression testing, manual testing and debugging. Chapter 7 introduces the most creative part of programming: design.

7.1 Introduction

7.2 The world-of-zuul game example

Exercise 1 - zuul-bad

In BlueJ

  1. Open project chapter07\zuul-bad
  2. From the class diagram at right, which class should you use to play the game?
  3. Explore and answer:
    1. What does the application do?
    2. What are the commands?
    3. What does each command do?
    4. How many rooms are there?
    5. Draw a map of just the rooms connected to the outside Room using the compass headings:

    N
W     E
    S

Exercise 2 - zuul-bad

In BlueJ

  1. From the class diagram above, open in the editor the class Room.
    • Change from Implementation to Interface.
    • According to your map, how were the outside Room exits defined?
       
  2. Open in the editor class Game.
    • Change to Implementation .
    • Examine the createRooms() method.
    • What is the effect of:
                lab.setExits(outside, office, null, null);
    • Finish your map.

7.3 Introduction to coupling and cohesion

 

7.4 Code duplication

Exercise 3 - Code duplication

In BlueJ

  1. The following Game methods have duplicated code.
    • Locate the duplication.
    • How can the duplication be eliminated?
    • Remove the duplication.
    • Test.
 private void printWelcome()
{
   System.out.println();
   System.out.println("Welcome to Adventure!");
   System.out.println("Adventure is a ....");
   System.out.println("Type 'help' if you need help.");
   System.out.println();
   System.out.println("You are " +
       currentRoom.getDescription());
   System.out.print("Exits: ");
   if(currentRoom.northExit != null)
      System.out.print("north ");
   if(currentRoom.eastExit != null)
      System.out.print("east ");
   if(currentRoom.southExit != null)
      System.out.print("south ");
   if(currentRoom.westExit != null)
      System.out.print("west ");
   System.out.println();
}
private void goRoom(Command command)
{
   if(!command.hasSecondWord()) {
      System.out.println("Go where?");
      return;
   }

   String direction = command.getSecondWord();

   // Try to leave current room.
   Room nextRoom = null;
   if(direction.equals("north"))
      nextRoom = currentRoom.northExit;
   if(direction.equals("east"))
      nextRoom = currentRoom.eastExit;
   if(direction.equals("south"))
      nextRoom = currentRoom.southExit;
   if(direction.equals("west"))
      nextRoom = currentRoom.westExit;

   if (nextRoom == null)
      System.out.println("There is no door!");
   else {
      currentRoom = nextRoom;
      System.out.println("You are " +
         currentRoom.getDescription());
      System.out.print("Exits: ");
      if(currentRoom.northExit != null)
         System.out.print("north ");
      if(currentRoom.eastExit != null)
         System.out.print("east ");
      if(currentRoom.southExit != null)
         System.out.print("south ");
      if(currentRoom.westExit != null)
         System.out.print("west ");
      System.out.println();
   }
}

7.5 Making extensions

A good design should allow extensions to be readily made. Coherent, loosely coupled classes are generally easier to extend than tightly coupled designs.

Consider your car's sound system. The system consists of several independent, loosely coupled modules (speakers, antenna, radio/player). A speaker is coherent because only speaker operations are part of the design . The speaker is loosely-coupled because speakers interface to the player only through standard plug-in connector and can easily be replaced.

Compare to an incoherent design where the turn-signals are are designed into the speaker. A tightly coupled design might have the radio tuning in the speaker system, changing speakers would require changing part of the radio system or changing the speakers to accommodate the radio. Better to have speakers and radio independent systems that communicate through a well-defined interface, the speaker connection.

7.5.1 The task

Add two new directions for room exits up and down.

7.5.2 Finding the relevant source code

The code defining and using room exits is in class Game methods (printLocationInfo and goRoom) and in class Room.

Defining Room class fields public allows the Game class direct manipulation of the fields, making the Room and Game class tightly coupled and difficult to change independent of the other. We will break the tight coupling.

class Game
{
    private Room currentRoom;

private void printLocationInfo()
{
   System.out.println("You are " +
      currentRoom.getDescription());
   System.out.print("Exits: ");
   if(currentRoom.northExit != null)
      System.out.print("north ");
   if(currentRoom.eastExit != null)
      System.out.print("east ");
   if(currentRoom.southExit != null)
      System.out.print("south ");
   if(currentRoom.westExit != null)
      System.out.print("west ");
   System.out.println();
}
private void goRoom(Command command)
{
   String direction = command.getSecondWord();

   Room nextRoom = null;
   if(direction.equals("north"))
      nextRoom = currentRoom.northExit;
   if(direction.equals("east"))
      nextRoom = currentRoom.eastExit;
   if(direction.equals("south"))
      nextRoom = currentRoom.southExit;
   if(direction.equals("west"))
      nextRoom = currentRoom.westExit;

   if (nextRoom == null)
      System.out.println("There is no door!");
   else {
      currentRoom = nextRoom;
      printLocationInfo()
   }
}
class Room
{
  public Room northExit;
  public Room southExit;
  public Room eastExit;
  public Room westExit;

  public void setExits(Room north,
                               Room east,
                               Room south,
                               Room west)
  {
      if(north != null)
            northExit = north;
      if(east != null)
            eastExit = east;
      if(south != null)
            southExit = south;
      if(west != null)
            westExit = west;
  }
Exercise 4 - Encapsulation and coupling

In BlueJ

  1. Edit the Room class.
    • Change field access from public to private.
    • The fields are now encapsulated within the Room class.
  2. Compile Room and Game classes.
  3. This tight coupling has been broken but must be fixed to allow adding other directions.

7.6 Coupling

After breaking the tight coupling between Room and Game classes, the compiler tells us where the coupling was broken because the northExit, etc. fields of Room are no longer accessible. Without  public access to Room fields we will define a Room accessor getExit() method that returns the room at an exit in a given direction. For example:

                        north east      south  west
outside.setExits(null,  theatre, lab,     pub);

outside.getExit("east");

returns the Room theatre.

Exercise 4.1 - Encapsulation and coupling
  1. Assign using setExits Room lab with north exit to the outside Room.
     
  2. What is returned by:

outside.getExit("west");

lab.getExit("west");

Several iterations of getExit() method - The following represents a iterative development of a design to achieve a more extensible design. The //1 design is the crudest, //2 represents some improvement and //3 represents the best design.

//1 implements the getExit() accessor method but requires adding more fields for each new direction and new if statements to setExits and getExit methods.

//2 uses a HashMap to avoid the need for multiple if statements in getExit() but setExits only allows four directions without changing the signature and adding a new field and if statements for each new direction.

//3 eliminates the need for any additional field or if statements. New directions can be added without changing the Room class. For example, any number of exits can be added by calling setExit() repeatedly:

outside.setExits(null, theatre, lab, pub);

outside.setExit("east", theatre);
outside.setExit("south", lab);
outside.setExit("west", pub);

with the resulting object diagram at right.

Exercise 4.2 - Encapsulation and coupling
  • Assign using setExit Room lab with north exit to the outside Room.

 

// 1

class Room
{
  private Room northExit;
  private Room southExit;
  private Room eastExit;
  private Room westExit;

  public void setExits(Room north,
                               Room east,
                               Room south,
                               Room west)
 {
      if(north != null)
           northExit = north;
      if(east != null)
            eastExit = east;
      if(south != null)
            southExit = south;
      if(west != null)
            westExit = west;
 }

 public Room getExit(String direction)
 {
      if(direction.equals("north")
         return northExit;
      if(direction.equals("east")
         return eastExit;
      if(direction.equals("south")
         return southExit;
      if(direction.equals("west")
         return westExit;
 }
// 2

class Room
{
  private HashMap exits;

  public void setExits(Room north,
                               Room east,
                               Room south,
                               Room west)
 {
      if(north != null)
            exits.put("north", north);
      if(east != null)
            exits.put("east", east);
      if(south != null)
            exits.put("south", south);
      if(west != null)
            exits.put("west", west);
 }

 public Room getExit(String direction)
 {
    return (Room)exits.get(direction);
 }
// 3

class Room
{
 private HashMap exits;

 public void setExit(String direction,
                             Room neighbor)
 {
    exits.put(direction, neighbor);
 }

 public Room getExit(String direction)
 {
   return (Room) exits.get(direction);
 }

 

 

Exercise 5 - Decoupling

In BlueJay

  1. Copy and paste the full version of the decoupled Room class // 3 below.
    • Compile Room and Game classes.
// 3
import java.util.HashMap;

class Room
{
  private String description;
  private String exitString;
  private HashMap exits;

 public Room(String description)
 {
    this.description = description;
    exitString = "";
    exits = new HashMap();
 }

 public void setExit(String direction,
                             Room neighbor)
 {
    exits.put(direction, neighbor);
    exitString = exitString + " " + direction;
 }

 public Room getExit(String direction)
 {
   return (Room) exits.get(direction);
 }

 public String getDescription()
 {
   return description;
 }

  public String getExitString()
 {
   return exitString;
 }
}

  1. Compiling Game class should give errors resulting from our decoupling changes to Room. Setting exits of the outside Room is below:
    • outside.setExit("east", theatre);
    • outside.setExit("south", lab);
    • outside.setExit("west", pub);
       
  2. Complete setting the theatre Room exits and comment out setting the remaining Room exits for now.
    • Compile Game, more errors.

 

Exercise 6 - Decoupling

In BlueJ

  1. Compile Game, more errors.
    • We have eliminated northExit, etc. in favor of the getExit() accessor method of Room.
    • The following returns Room theatre for the Room at the "east" exit of outside Room.

    outside.setExit("east", theatre);
    outside.setExit("south", lab);
    outside.setExit("west", pub);

    outside.getExit("east");

  2. What are the changes to use getExit() rather than northExit in:
     
    • if(direction.equals("north"))
            nextRoom = currentRoom.northExit;
       
  3. Make the changes to the goRoom() method of Game.
  4. Compile Game.
private void goRoom(Command command)
{
   String direction = command.getSecondWord();

   Room nextRoom = null;
   if(direction.equals("north"))
      nextRoom = currentRoom.northExit;
   if(direction.equals("east"))
      nextRoom = currentRoom.eastExit;
   if(direction.equals("south"))
      nextRoom = currentRoom.southExit;
   if(direction.equals("west"))
      nextRoom = currentRoom.westExit;

   if (nextRoom == null)
      System.out.println("There is no door!");
   else {
      currentRoom = nextRoom;
      printLocationInfo()
   }
}

 

Exercise 7 - Decoupling

In BlueJ

  1. Compile Game, more errors.
    • We have eliminated northExit, etc. which decouples but breaks the printLocationInfo() method at:

      if(currentRoom.northExit != null)
            System.out.print("north ");
       

  2. We need to maintain a list of exits for each Room. The following sets the exits for the outside Room.

    outside.setExit("east", theatre);
    outside.setExit("south", lab);
    outside.setExit("west", pub);

  3. It is a simple matter to add a Room field named exitString that accumulates by concatenating the direction String for each exit.
    • An accessor method getExitString() returns the String field exitString.
    • For example:

      outside.getExitString() returns "east south west".
       

    • The changes have been made to Room class. Examine.
       
  4. What are the changes to use getExitString() rather than northExit in:
     
    •    if(currentRoom.northExit != null)
            System.out.print("north ");
       
  5. Make the changes to the printLocationInfo() method below (i.e. your method created to remove the duplication) of Game.
     
  6. Compile Game. Does that resolve the explicit coupling errors?
private void printLocationInfo()
{
   System.out.println("You are " +
      currentRoom.getDescription());
   System.out.print("Exits: ");
   if(currentRoom.northExit != null)
      System.out.print("north ");
   if(currentRoom.eastExit != null)
      System.out.print("east ");
   if(currentRoom.southExit != null)
      System.out.print("south ");
   if(currentRoom.westExit != null)
      System.out.print("west ");
   System.out.println();
}

7.7 Responsibility-driven design

A class should handle its own data, as we saw with the northExit, eastExit etc. making data accessible outside a class increases coupling.

7.7.1 Responsibilities and coupling

Adding other directions has been made simple through our earlier exercises. But suppose we want to place treasure, weapons or monsters in the rooms and be able to add or remove items as the game progresses. 

Exercise 8 - Responsibilities

The current definition of printLocationInfo() would need to be changed if Room added other information to be printed. For the Room class to be responsible for its own data, it should return a full description of the Room data.

private void printLocationInfo()
{
   System.out.print("You are " +
      currentRoom.getDescription());

   System.out.println("Exits: " +
      currentRoom.getExitString());

}

In BlueJ

  1. The Room class would need an accessor method at right to return a full description of a Room contents, exits, etc.
     
  2. Make changes needed to printLocationInfo() method (i.e. your method created to remove the duplication) of Game.
class Room
{
   public String getLongDescription()
  {
      return "You are " + description + ".\n" +
                getExitString();
  }


7.8 Localizing change

localizing change - Changing one class should only minimally effect other classes. Promoted by loose coupling and high cohesion. Design classes with modification and extension in mind. 

7.9 Implicit coupling

implicit coupling - Coupling that may not produce a syntax error when changes occur. 

Use of public fields has obvious potential for tight coupling that leads to syntax errors when fields change in some way. Implicit coupling is often due to duplication of data throughout the application. Data should occur only once in an application.

The game commands are:

The data for recognizing commands in the CommandWords class with "look" added is:

class CommandWords
{
   private static final String validCommands[] =
       { "go", "quit", "help", "look" };

The same data is also in the Game class printHelp() method:

class Game
{
  private void printHelp()
 {
    System.out.println("You are lost. You wander");
    System.out.println("around at the university.");
    System.out.println();
    System.out.println("Your command words are:");
    System.out.println(" go quit help");
 }

Implicit coupling - Adding more command words in CommandWords class requires changing Game also.

We could add a CommandWords method to return a String of all the command words and call it from printHelp(). But the class diagram indicates the Parser class already depends on the CommandWords class, making Game depend on CommandWords would increase coupling.

Good designs reduce coupling. Better to define a Parser method to return the command words from the CommandWords class instead.

class Game
{
 private Parser parser;

 private void printHelp()
{
  System.out.println(
                 "You are lost.");
  System.out.println(
                 "Your command words are:");
  System.out.println(
                 parser.getCommands());
}
class Parser
{
 private CommandWords
                            commands;

 public String getCommands()
 {
    return
        commands.getWords();
 }
class CommandWords
{
 private static final String
  validCommands[] =
    { "go", "quit", "help", "look" };

 public String getWords()
 {
  String words = "";
  for(int i=0; i<validCommands.length; i++)
              words = words + " " +
                                     validCommands[i];
  return words;
 }     

 

Exercise 9
  • Examine the following Game method. Does implicit coupling still exist with CommandWords class?
     
  • What is needed to add the "look" command?
class Game
{
 private boolean processCommand(Command command)
{
   boolean wantToQuit = false;

   if(command.isUnknown()) {
      System.out.println("I don't know what you mean...");
      return false;
   }

   String commandWord = command.getCommandWord();
   if (commandWord.equals("help"))
      printHelp();
   else if (commandWord.equals("go"))
            goRoom(command);
         else if (commandWord.equals("quit"))
                  wantToQuit = quit(command);

   return wantToQuit;
 }


7.10 Thinking ahead

Model/View/Controller - models for maintaining data, views for displaying the data, and controllers for manipulating the model.

7.11 Cohesion

7.11.1 Cohesion of methods

method cohesion - A cohesive method is responsible for only one task.

 

class Game
{
  public void play()
  {
     System.out.println();
     System.out.println("Welcome to Adventure!");
     System.out.println("Adventure is a new, incredibly boring adventure game.");
     System.out.println("Type 'help' if you need help.");
     System.out.println();
     printLocationInfo();

     currentRoom = outside;

     boolean finished = false;
     while (! finished) {
          Command command = parser.getCommand();
          finished = processCommand(command);
     }
     System.out.println("Thank you for playing. Good bye.");
  }
}
Exercise 10 - Cohesion of methods
  • Is the above play() method cohesive?
  • Are there any changes that would make it more cohesive?

7.11.2 Cohesion of classes

class cohesion - A cohesive class is responsible for one thing.
Exercise 11 - Cohesion of classes
  1. Is our text book more or less cohesive than the National Inquirer?
  2. Would adding a text chapter about a meeting between space aliens and George Bush produce more or less cohesion?
  3. Would adding a Parser class method to manipulate the Room state or adding a Room field to the Parser:
    1. Add an arrow from Room to Parser in the class diagram?
    2. Increase or decrease coupling between Parser and Room classes?
    3. Increase or decrease class cohesion of Parser?
    4. Increase or decrease class cohesion of Room?

7.11.3 Cohesion for readability

Exercise 12 - Cohesion for readability
  1. How are the Parser and Room classes coupled? Loosely, tightly or not at all?
  2. Would it make sense to read the Parser class to understand the Room class?

7.11.4 Cohesion for reuse

reuse - Multiple use of the same class or method (i.e. calling the method more than once)..
class Game
{
  private void printWelcome()
 {
    System.out.println("Welcome to Adventure!");
    System.out.println("Type 'help' if you need help.");
    System.out.println();
    System.out.println(currentRoom.getLongDescription());
  }

  private void goRoom(Command command)
  {
    if (nextRoom == null)
        System.out.println("There is no door!");
    else {
        currentRoom = nextRoom;
        System.out.println(currentRoom.getLongDescription());
    }
  }
}

 

Exercise 13 - Cohesion for reuse
  • What is reused in the above fragment?

7.12 Refactoring

refactoring - Adapting existing classes and methods to meet new requirements. Often adds functionality at the expense of more complex classes or methods.
Exercise 14 - Refactoring

Add items (money, monsters, etc.) of varying weights placed in rooms.

  1. What data (fields) is needed for an item?
     
  2. What methods are needed to manipulate the fields?
     
  3. Where are the most obvious points for refactoring to have different items in a room?
    • Will the Room class need refactoring?
    • Will Game class need refactoring?
    • Should a new Item class be defined with responsibility for the items?

7.12.1 Refactoring and testing

Exercise 15 - Refactoring and testing
  • Does a class that tests other classes need to be refactored when:
    • additional methods are added to classes tested?
    • methods are removed from classes?
    • signatures of methods tested change?
    • signatures of methods tested do not change?

7.12.2 An example of refactoring

Exercise 16 - Refactoring example

Add ability for the player to pick up and put down items found in rooms.

  1. What actions (methods) are needed to manipulate item?
     
  2. Should an Item pick itself up or should a player pick up an Item?
     
  3. Should the Item class have responsibility that includes a player carrying an item?
     
  4. Rate the following from most (1) to least (5) appropriate:
    1. Room class is refactored with responsibility that includes items a player is carrying.
    2. Game class is refactored with responsibility that includes items a player is carrying.
    3. Item class is refactored with responsibility that includes a player carrying an item.
    4. Player class is created with responsibility that includes items a player is carrying.
    5. Parser class is refactored with responsibility that includes items a player is carrying.
       
  5. Will Game class need refactoring?

7.13 Design guidelines

7.14 Executing without BlueJ

7.14.1 Class methods

instance method - Methods invoked on an instance (object) of a class.
class method - Methods invoked on the class.

7.14.2 The main method

class Game
{
   public static void main(String args[])
  {
      ( new Game() ).play();
  }
}

7.14.3 Limitations of class methods



C201 Home Page


Please send any comments to: jfdoyle@ius.edu 
Copyright 2001-2004 by cgranda@ius.edu . All rights reserved.