(Coursenotes for CSC 203 Project-based Object-oriented Programming and Design)

Interfaces (part 2)

In the previous lesson, we learned about interfaces — both the general concept as well as the program construct in Java. In this lesson, we’ll walk through an example of creating and using interfaces in a program design process. We’ll see how interfaces can help reduce coupling and introduce separation of concerns.

Updating the Nim Game

Will work with the Nim game example that we talked about in the lesson on a class design process. So far, we have a game that only supports human players, i.e., at each turn our Game class will pause and wait for a Player to manually enter the number of sticks they want to pick up.

In this lesson we’ll add support for more kinds of automated players, i.e., bots that the human player can play with.

Please take a minute to go look at the current implementation of our Nim game. We have the following class structure that accomplish the following tasks. The relationship between the classes here is a has a relationship. I.e., the Game class has two Players, and it has a Pile as global members (instance variables, or tantamount to instance variables).

classDiagram
  direction LR
  note for Game "Underlined members are static." 
  Game --> Player : has two
  Game --> Pile : has a
  class Game {
    +Player p1
    +Player p2
    +Pile pile
    playGame() void$
  }

  class Player {
    +String name
    +getName() String
    +takeTurn(Pile) int
  }

  class Pile {
    +int numSticks
    +removeSticks(int) void
    +getSticks() int
  }

We now want to create support for including multiple types of Players — not just “human” Players where the game must pause and wait for input from the user.

Ideally, we would like to do this without having to update the Game logic too much.

Our strategy

We can do this by creating a Player interface. The Game will still interact with a Player, just like it has thus far.

First, we can start by defining a Player interface with two abstract methods.

classDiagram
  note for `interface Player` "Italicised methods are abstract."
  note for `interface Player` "The takeTurn method returns the number of sticks that were removed."
  class `interface Player`{
    +getName() String*
    +takeTurn(Pile) int*
  }

Then, once our Player interface is created, we can refactor our Game class to only use behaviours that the Player interface supports, i.e., to only depend on behaviours that all players can perform, like taking a turn and returning one’s name. As far as the Game is aware, both p1 and p2 are just Players — but at runtime, they might be any one of the following:

Implementation

The Player interface

Here is our Player interface.

public interface Player {
  String getName();
  int takeTurn(Pile pile);
}

Notice a change from our previous implementation. Previously, our Player’s takeTurn method expected as a parameter the number of sticks to remove from the pile. Now, we let each Player compute the number of sticks to remove, and we give that information back to the Game. Can you think of why we’ve made this change? We will discuss this further below.

The Timid Player

The TimidPlayer always removes one stick from the pile of sticks.

public class TimidPlayer implements Player {
  private String name;
  
  public TimidPlayer(String name) {
    this.name = name;
  }

  @Override
  public String getName() {
    return name;
  }

  @Override 
  public int takeTurn(Pile pile) {
    pile.removeSticks(1);
    return 1;
  }
}

The Greedy Player

Recall that implementing subclasses of the same interface don’t have to have all the same instance variables.

The interface defines a “lower bound” on what the class must implement. The class must implement the methods declared in the interface, but it can also implement additional behaviours.

Our GreedyPlayer has one additional behaviour in addition to what is required by the Player interface. The GreedyPlayer is not the sharpest tool in the shed, and in addition to its less-than-optimal gmae play strategy, it also likes to antagonise its opponent.

So we give the GreedyPlayer a jeer instance variable.

public class GreedyPlayer implements Player {
  private String name;
  private String jeer; // This player talks smack
  
  public GreedyPlayer(String name, String jeer) {
    this.name = name;
    this.jeer = jeer;
  }

  public void jeer() {
    System.out.println(this.jeer);
  }

  @Override
  public String getName() {
    return this.name;
  }

  @Override  
  public int takeTurn(Pile pile) {
    int toRemove = 0;
    if (pile.getSticks() >= 3) {
      toRemove = 3;
    } else {
      toRemove = pile.getSticks();
    }
    return toRemove;
  }
}

The Random Player

Our RandomPlayer uses a Random object to generate a random number of sticks to pick up each time.

import java.util.Random;

public class RandomPlayer implements Player {
  private String name;
  private Random random;
   
  public RandomPlayer(String name) {
    this.name = name;
    this.random = new Random();
  }

  @Override
  public String getName() {
    return this.name;
  }

  @Override 
  public int takeTurn(Pile pile) {
    // If there's more than 3 sticks on the pile, only remove 1--3 sticks.
    // If there's fewer than 3 sticks on the pile, don't try to remove more
    // than the remaining number of sticks.
    int toRemove = this.random.nextInt(1, Math.min(3, pile.getSticks()) + 1);
    pile.removeSticks(toRemove);
    return toRemove;
  }
}

The Game class

With all of that set up, let’s think about how the Game looks now. (We’ll come back to the HumanPlayer after this.)

Use the “Walkthrough” button to step through the code below. Take time to read the code and understand what is going on.

The key thing to note here is that the Game functions the same way no matter how many different kinds of Player subtypes we support.

View in new tab

The Human Player

Finally, let’s look at the HumanPlayer. We’re going to do this bit as an in-class discussion.

In the previous implementation of the Game, the Game was responsible for deciding how many sticks to pick up, and then giving that information to the Player object by calling the takeTurn method. However, that meant that the Game logic was coupled with the Player logic — it knew about the player’s strategy for choosing a number of sticks to pick up (i.e., ask the user and wait for input).

In our current implementation, we’ve introduced a degree of separation between Game logic and Player logic, setting things up so the Game can be totally unaware of how the Player takes their turn. This allowed us to incorporate three different types of Players, each with their own turn taking strategies.

How do we incorporate the HumanPlayer into this class structure? Discuss this in class.

Here are some hints to keep in mind as you think through this (click to expand).

Hint 1

The Game has a Scanner object that is setup to accept input that the user types in, i.e., System.in.

Hint 2

It is considered good practice to not create multiple Scanner objects for the same input stream. So we need to use this same Scanner object in the HumanPlayer class.

Hint 3

We need to pass that Scanner object to the HumanPlayer so that the HumanPlayer can use it, while still making it adhere to the Player interface.

Introducing player-specific functionality

In Greedy player implementation above, we included an additional instance variable for the GreedyPlayer — the jeer. Suppose we want our GreedyPlayers to “talk smack” every time they play a turn, i.e., we want to them to print their jeer each time they take a turn.

I will work through two ways in which to add this behaviour, and we will discuss pros and cons of each strategy.

#1 The instanceof operator

Strategy 1 is to make the Game handle this behaviour. Whenever a player plays a turn (in the play method of the Game), we check if the player is an instance of GreedyPlayer. That is, even though the static type of p1 and p2 is Player, we can check at run time if their dynamic type is GreedyPlayer.

We can do this using the instanceof operator.

The instanceof operator works with a variable and a data type, and checks—at run time—if the variable is an instance of that data type.

Below is the play method of the Game class, reproduced with a few added lines of code.

View in new tab

#2 Make the GreedyPlayer do it

Strategy 2 is to make the GreedyPlayer handle this behaviour.

The GreedyPlayer already knows what kind of player it is—dynamic dispatch is already taking care of calling the right takeTurn method depending on the player type. So since we want this behaviour to take place each time the GreedyPlayer takes a turn, we could change our GreedyPlayer’s takeTurn method to the following.

public class GreedyPlayer implements Player {
  // Rest of the class stays the same...
  @Override  
  public void takeTurn(Pile pile) {
    int toRemove = 0;
    if (pile.getSticks() >= 3) {
      toRemove = 3;
    } else {
      toRemove = pile.getSticks();
    }

    // ADDED: Talk smack
    System.out.println(this.jeer);

    return toRemove;
  }
}

Discuss. What are some pros and cons of the two approaches above? Which one do you prefer, and why?

Summary

By using interfaces, we have introduced a degree of separation of concerns between the Game and the Player. The Game interacts with two Player objects. Those objects may, at run time, be any one of several possible Player subtypes.

Do you remember what the ability of a variable to be take many possible forms at run time is called?

The Game doesn’t know or care about this, since it only knows about the Player interface.

The diagram below shows the entire system using a somewhat informal flowchart notation. Note that the diagram is showing both has-a relationships (wherein one class has instances of another class as instance variables), and is-a relationships (wherein one or more classes are subclasses of another class or interface).

classDiagram
  direction LR
  note for Game "Underlined members are static." 
  Game --> `interface Player` : has two
  Game --> Pile : has a
  `interface Player` <|-- TimidPlayer : is a 
  `interface Player` <|-- GreedyPlayer : is a 
  `interface Player` <|-- RandomPlayer : is a 

  class Game {
    +Player p1
    +Player p2
    +Pile pile
    playGame() void$
  }

  class `interface Player` {
    +getName() String*
    +takeTurn(Pile) int*
  }

  namespace PlayerSubtypes {
    class TimidPlayer { }

    class GreedyPlayer {
      +String jeer
      +jeer() void
    }

    class RandomPlayer { }
  }

  class Pile {
    +int numSticks
    +removeSticks(int) void
    +getSticks() int
  }