Chapter 7
Designing Classes

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 starting Room, outside the main entrance of the university. Use the compass headings:

    N
W     E
    S

Exercise 2 - zuul-bad

In BlueJ

  1. Open in the editor the class Room.
     
    • Select Tools | Toggle Documentation View.
       
    • Read the documentation to determine the following:
       
      • How is a Room created (i.e. constructed)?
         
      • What are the fields?
         
      • How are exits to a Room defined?
         
    • According to your map, how were the outside the main university entrance Room exits defined (i.e. the method call)?
       
  2. Open in the editor class Game.
     
    • Examine the createRooms() method.
       
    • What is the effect of:
                lab.setExits(outside, office, null, null);
       
    • Finish your map for the complete game.


7.3 Introduction to coupling and cohesion

 


7.4 Code duplication

Exercise 3 - Code duplication

In BlueJ

  1. The following two Game methods duplicate code.
    • Locate the duplication.
    • How can the duplication be eliminated?
    • Remove the duplication in zuul-bad.
    • Test.
  2. Is code duplication bad? Explain.
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 (producing sound) are part of the design; not heating or cooling. 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 and air-conditioner are designed into the speaker.

A tightly coupled design example is the boom-box. The radio tuning is part of a tightly coupled system, where if one part fails the entire system must be replaced. 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.

The Room and Game class are 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, tight-coupling is broken.
       
  2. Compile Room and Game classes. Explain the results.
     
  3. This tight-coupling has been broken but must be fixed. We'll replace with loose-coupling to simplify adding new directions.


7.6 Coupling

After breaking the tight-coupling between Room and Game classes, the compiler tells us where the coupling was broken.

The northExit, etc. fields of Room are no longer accessible outside of the Room class. These fields are encapsulated in the Room class by defining as private.

Rather than public access to Room fields directly 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 now 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 more loosely-coupled and extensible design.

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

//2 represents some improvement, uses a HashMap to avoid the need for multiple if statements in getExit().

//3 represents the best design. Eliminates the need for any additional field or if statements. Most importantly, new directions can be added without changing the Room class.

The key difficulty is that setExits signature only allows four directions. Adding a new direction requires changing the signature, adding a new field and if statements.

setExit() instead adds only one exit at a time, eliminating fixing number of directions in the signature. Any number of exits can be added by calling setExit() repeatedly. The following produces the resulting object diagram at right.

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

 

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

 

 

// 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

import java.util.HashMap;

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);
 }

 

 

// 3      Room

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;
 }
}

Exercise 5 - Decoupling

In BlueJay

  1. Copy and paste the full version of the decoupled Room class // 3 below.
    • Compile Room and Game classes.
  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.

 

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 6 - Decoupling

In BlueJ

  1. Compile Game, more errors.
    • We have eliminated public access to northExit, etc. in favor of the getExit() accessor method of Room, decoupling Game goRoom().
    • 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. For Game goRoom(), what are the changes to use getExit() rather than northExit in:
     
    • if(direction.equals("north"))
            nextRoom = currentRoom.northExit;
       
  3. Make the appropriate changes to the goRoom() method of Game.
  4. Compile Game.

 

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();
}
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 ");
       

    • We could use currentRoom.getExit("north") but requires changes with each new direction. More importantly, exits are part of a room and should be the responsibility of Room class.
       
  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. We can maintain the exit list by adding a String field named exitString to Room class and accumulating exit directions 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 Room class.
       
  4. What are the necessary changes in printLocationInfo() to use getExitString() rather than explicit coupling in:

       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 ");
     

  5. Make the changes to the printLocationInfo() method (i.e. your method created to remove the duplication) of Game.
     
  6. Compile Game. Does that resolve the explicit coupling errors?

 

Exercise 7.5 - Completing the up and down extension.

In BlueJ

In Game class createRooms() method:

  1. Add a Room, earth with appropriate description.
  2. Set exit of the outside to earth.
  3. Test that outside Room has the new exits.

 

7.7 Responsibility-driven design

A class should handle its own data, as we saw with the fields northExit, eastExit etc. defined public in Room making data directly 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() in Game class 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 with "look" added:

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 look");
 }

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 as in the second diagram.

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
{
 public void play()
 {
   printWelcome();
   boolean finished = false;

   while (! finished) {
      Command command = parser.getCommand();
      finished = processCommand(command);
   }
   System.out.println("Thank you for playing. Good bye.");
 }

 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
     
  • 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 chapter about a recent meeting between space aliens and Barack Obama 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. Refactor Room.

  1. What data (fields) is needed for a Room to contain items named gold and silver?
     
  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

   public static void main(String args[])
  {
      ( new Game() ).play();
  }
Exercise 17 - Compiling and executing Java from the command line
  • IUS - The Game class method can be executed by:
    • Start | Programs | Accessories | Command Prompt
    • Change to the directory containing the Java Game class files.
      • C:
         
      • cd \Users\username\blueJ\chapter07\zuul-better
         
      • javac  Game.java
         
      • java Game


7.14.3 Limitations of class methods