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 Player
s, 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 Player
s — 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 Player
s — but at runtime, they might be any one of the following:
GreedyPlayer
— In thetakeTurn
method, theGreedyPlayer
always takes as many sticks as possible, i.e., 3 if available, or as many sticks as there are left on the pile. Clearly not a winning strategy.TimidPlayer
— This player always the fewest possible number of sticks, i.e., 1.RandomPlayer
— Just for fun, this player picks randomly between 1, 2, and 3 sticks, and takes those many sticks from the pile.HumanPlayer
— This is the player we implemented last week.
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.
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 GreedyPlayer
s 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.
#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 }