Lecture 6: The Model
Suppose you wanted to implement a graphical game. Where would you start, and how would you structure your program? A common object-oriented technique for structuring graphical programs is the model—view—controller pattern, which separates the program into three distinct but cooperating components,
the controller, which takes input from the user and decides what to do,
the view, which knows how to display the game interface to the user,
the model, which represents the state of the game internally and (often) enforces its rules.
It can sometimes be mysterious what this means in practice, and we will get to that eventually. A reasonable place to start, when both designing and learning, is the model, so that is our subject for this lecture.
1 An introductory example: Tic-tac-toe
For the unfamiliar, Tic-tac-toe is a two-player game played on a 3-by-3 grid, where the players alternately mark cells of the grid. A player wins by making a line of three marks, or if the grid fills with no winner then the result is a tie. For example, here a sequence of game states in which player “X” wins:
│ │ X│ │ X│ │O X│ │O X│ │O X│ │O X│ │O X│ │O
─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─
│ │ │ │ │ │ │ │ │O│ │O│ │O│ X│O│
─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─ ─┼─┼─
│ │ │ │ │ │ │ │X │ │X X│ │X X│O│X X│O│X
1.1 Object-oriented analysis
In order to design a model for a Tic-tac-toe game, we need to consider what operations a client of our model will need to perform to manipulate and query the game state. What do we need to be able to do? These operations should suffice:
Play a move as X.
Play a move as O.
Find out whose turn it is.
Find out the contents of the grid, perhaps in order to display it.
Find out whether the game is over, and if so, who the winner is, if any.
Because we want the model to enforce the rules of the game, we should also consider ways in which clients should not be able to manipulate the model. Rather than allow the model’s state to be corrupted, it should signal an exception in these cases:
Attempting to play out of turn.
Attempting to play in a cell that already has a mark.
Attempting to play after the game has ended.
Attempting to play in a cell that doesn’t exist (if the interface allows that to be expressed).
Having completed our analysis, we can now begin to design an interface for the Tic-Tac-Toe model.
1.2 Designing an interface
We have a number of choices for how to realize the operations above as methods in Java. Let’s start with a method or methods for playing moves.
1.2.1 How to move
In one possible design, we could provide separate methods for playing as X and playing as O:
/**
* Places an X mark in the specified cell.
*
* @param column the column of the cell
* @param row the row of the cell
* @throws IllegalStateException if it's Y's turn, if the game is
* over, or if there is already a mark in the cell.
* @throws IndexOutOfBoundsException if the cell is out of bounds.
*/
void moveAsX(int column, int row);
/** [mutatis mutandis] */
void moveAsY(int column, int row);
With the above design, the model must throw an exception if its client
calls moveAsX
when it is Y’s turn or moveAsY
when it is X’s turn.
Alternatively, because the model must track whose turn it is, we could simply have one move method, and require the model to move for the correct player in each case:
/**
* Places an X or O mark in the specified cell. Whether it places an X
* or O depends on which player's turn it is.
*
* @param column the column of the cell
* @param row the row of the cell
* @throws IllegalStateException if the game is over, or if there is
* already a mark in the cell.
* @throws IndexOutOfBoundsException if the cell is out of bounds.
*/
void move(int column, int row);
Of course, there are additional possibilities such as a single move
method that takes some specification of which player is moving as the
parameter. And we may not have specified the method fully, since we
haven’t said how the column and row coordinates map onto the grid. (Are
they zero- or one-based? How are they oriented?1Tic-tac-toe has
both rotational and reflective symmetries, which means that the
orientation doesn’t actually matter so long as the UI is consistent.)
1.2.2 Whose turn is it?
In order to allow the client to find out whose turn it is, we also have several choices. One simple way is a boolean method or methods to ask if it is a particular player’s turn:
/**
* Determines whether it is player X's turn to move.
*
* @return whether X can play now
*/
boolean isXsTurn();
/** [mutatis mutandis] */
boolean isYsTurn();
Even within the above approach, we have a design choice: What should the
methods do if called once the game is over? They could return
false
, or they could throw an exception; either way, we ought
to document this choice. Which do you prefer, and why?
Another way would be to have the method return some representation of the player whose turn it is, like so:
/**
* Returns the player whose turn is next.
*
* @return the next player
* @throws IllegalStateException if the game is over
*/
? nextPlayer();
There are a variety of choices we could use for the return type ?
.
Which of these do you prefer, and why?:
type
char
, with'X'
for X and'Y'
for Yclass
String
, with"X"
for X and"Y"
for Ytype
boolean
, withtrue
for X andfalse
for Ytype
boolean
, withtrue
for Y andfalse
for Xan enumeration defined as
enum Player { X, Y }
Using type char
is a poor design choice, because the
char
type has many other values that don’t stand for valid
players. Java’s type system will not help us in preventing nextPlayer
from returning meaningless values—so then what should we do if it does?
More to the point, the char
type is good for representing
textual characters, and (ab)using it for other meanings is unstylish.
Using a String
is even worse, because it has all the same drawbacks as
using a char
, except it has even more possibilities, including
null
, the empty string, and longer strings. (Important
guideline you should always follow: Strings are for representing textual
information, where the possible values are many or unlimited and not
known ahead of time. If you find yourself using strings to represent
some small set of values, or for any kind of internal API communication
that is never presented to the user, you are doing it wrong.)
The boolean
idea solves the main problems that we saw with
char
and String
: there are exactly two Boolean values,
and we need two values. However, boolean
could be confusing,
because which player is true
and which false
is
non-obvious. In general, we should use boolean
s only when the
value we are trying to represent is a truth value.
Finally, we come to the enumeration, which declares a new class Player
having exactly two values, Player.X
and Player.Y
. This
expresses very clearly what the possibilities are and what they mean.
One caveat with enumerations, though: switch
statements over
enumerations always require a default
case, even if every
current possibility appears in some other case. This is because Java
assumes an open world, in which more values can be added to an
enumeration at a later time.
1.2.3 Getting the grid
We want our model to enforce the rules of the game. This is not because
we expect untrustworthy users to interact with it directly—
In order to enforce the rules and prevent client code from corrupting the state of the grid, we need to ensure that all changes to the state happen via our model class’s public methods, which will enforce the rules. Thus, we cannot merely return a reference to our internal mutable representation of the grid to the client, because then the client could change it. However, we can safely return a copy of the grid (which need not be the same type as what we use internally):
/**
* Returns a two-dimensional array representing the state of the
* grid. The first index is the column and the second the row.
* The resulting array is newly allocated and unconnected to the model;
* thus, mutating it will have no effect on the model or subsequent
* calls to {@code getGrid}.
*
* @return a copy of the grid
*/
Player[][] getGrid();
Alternatively, rather than return some representation of the grid, we could provide some means of querying it:
/**
* Returns the {@link Player} whose mark is in the cell at the given
* coordinates, or {@code null} if that cell is empty.
*
* @param column the column of the cell
* @param row the row of the cell
* @return a {@code Player} or {@code null}
* @throws IndexOutOfBoundsException if the cell is out of bounds.
*/
Player getMarkAt(int column, int row);
1.2.4 Finding out the results
Finally, we need some way to find out when the game is over and who, if anyone, won. Unlike the previous operations, there isn’t much room in this design space (though there are a few sensible alternatives). We will have one method to ask whether the game is over and another to ask who has won:
/**
* Determines whether the game is over.
*
* @return whether the game is over
*/
boolean isGameOver();
/**
* Returns the winner of the game, or {@code null} if the game is a
* tie.
*
* @return the winner or {@code null}
* @throws IllegalStateException if the game isn't over
*/
Player getWinner();
In a game that does not admit ties, could these methods be combined into one? Is that a good idea?
1.3 Implementation
Tic-tac-toe is a very simple game, yet designing the interface to the model can still be an involved process. Some decisions are consequential, some not, and we might not be able to tell which is which until we start attempting to implement or even use the interface. However, whatever interface design we might choose, much of the hard work is now done, and implementing the interface is a simple matter of programming.
2 A bigger example: Connect $N$
As our second example, we consider the game Connect $N$, which is a generalization of the game Connect Four. In Connect Four, two players take turns dropping tokens (white for one player and red for the other) into a vertical 7-by-6 grid. The first player to form a line of four or more of their tokens, horizontally, vertically, or diagonally, wins the game. For Connect $N$, we will allow the dimensions of the grid, the line length necessary to win, and the number of players to be configurable rather than fixed.
For example, here is a sequence of game states for two-player, 3-by-4 Connect Three in which Player W wins:
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │W│ │ │ │W│ │
│ │ │ │ │W│ │ │ │W│R│ │ │W│R│ │ │W│R│R│
└─┴─┴─┘ └─┴─┴─┘ └─┴─┴─┘ └─┴─┴─┘ └─┴─┴─┘
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │W│
│W│W│ │ │W│W│ │ │W│W│R│ │W│W│R│
│W│R│R│ │W│R│R│ │W│R│R│ │W│R│R│
└─┴─┴─┘ └─┴─┴─┘ └─┴─┴─┘ └─┴─┴─┘
2.1 Object-oriented analysis
We begin our design once again by considering what operations the model must provide and what exceptional conditions the operations will detect. The operations are somewhat similar to Tic-tac-toe’s operations. We need ways to:
Play a move as a particular player. (How will we represent players when the number isn’t fixed at two?)
Find out about the configuration parameters of the game, including:
the dimensions of the grid,
the goal line length needed to win, and
who the players are.
Find out about the state of the game, including:
whose turn it is,
the contents of the grid,
whether the game is over,
and if so, who the winner is, if any. (Connect $N$, like Tic-tac-toe, allows for ties.)
We need to reject bad moves, including playing out of turn, playing in a non-existent column, or playing in a column that is already full to the top. Additionally, we may want to make it an error to ask for the winner before the game is over, or to ask for the next player after the game is over.
2.2 Interface design
Because several methods will deal with players, one of the first
decisions we must make is how to represent the players and their tokens.
One simple way would be with integers counting up from 0. However, we
will make the model slightly richer by associating a name, represented
as a String
, with each player. The set of players and their
turn order will be represented as a String[]
, with most methods
representing each player as an index into that array.
2.2.1 How to move?
To take a turn, it is sufficient to select which column the current
player wants to play in. However, as an extra check, we will have the
move
method also take the int
representing the player who is
moving, which the model then checks before allowing the move. Thus:
/**
* Plays a move. Given the player (whose turn it must be) and the column
* number (zero-based from the left), attempts to add a token for that
* player to that column. If this move ends the game then the game state
* is updated to reflect that. Because it may be useful to the client,
* this method returns the row number where the token lands.
*
* @param who the player who is moving
* @param where which column to play in
* @return the row where the token ends up
*
* @throws IllegalStateException if the game is over
* @throws IllegalStateException if it isn't {@code who}'s turn
* @throws IllegalStateException if the requested column is full
* @throws IndexOutOfBoundsException if the requested column does not exist
*/
int move(int who, int where);
2.2.2 Querying the configuration
Although presumably the client must specify the game configuration when instantiating the game model, it may be useful to allow the client to ask the model for this information. (Why?) Thus, we will have methods that return the width and height of the grid, the goal line length, and the sequence of players:
int getWidth();
int getHeight();
int getGoal();
String[] getPlayers();
It’s important that getPlayers()
return a copy of the array
held by the model object rather than the array itself, since returning
the array itself would then allow the client to modify the model’s
private state.
2.2.3 Querying the game state
The game is always in one of three different statuses:
Playing, when the game isn’t over yet,
Stalemate, when the game has ended in a tie, and
Won, when the game has ended with a winner.
In order to communicate these statuses to the client, we will declare an enumeration within the game model class:
public static enum Status { Playing, Stalemate, Won }
getStatus
method; as a convenience
we will also define an isGameOver
method:Status getStatus();
/**
* Determines whether the game is over.
*
* @return whether the game is over
*/
boolean isGameOver();
When the game isn’t over, we can ask which player’s turn it is, and when the game has a winner, we can ask who the winner is:
/**
* Returns the winner of the game.
*
* <p><strong>PRECONDITION:</strong> the game is over and has a winner
*
* @return the winner
* @throws IllegalStateException if {@code getStatus() != Status.Won}
*/
int getWinner();
Finally, we need a way for the client to observe the contents of the grid. This
time we will provide a way to query the token at any given cell position.
Because some cells may be empty of tokens, we need some way to distinguish
empty cells from player numbers. The wrapped integer type Integer
includes null
, so we use Integer
instead of int
for
the result with null
representing empty cells.
Additionally, we will include a convenience method3Meaning that the client could do what this method does using the other public methods, but it’s still nice to have. for asking whether a particular column is full.
/**
* Gets the player whose token is at the given column and row. The
* coordinates are zero-based and start in the lower left. Returns
* {@code null} if there is no token in the given position.
*
* @param x the column coordinate ({@code 0 <= x < width})
* @param y the row coordinate ({@code 0 <= y < height})
* @return the player in the given position, or {@code null}
* @throws IndexOutOfBoundsException if (x, y) is out of bounds
*/
Integer getPlayerAt(int x, int y);
/**
* Determines whether the specified column is full and thus cannot be played
* in.
*
* @param which the column to check
* @return whether column {@code which} is full
*/
boolean isColumnFull(int which);
3 Configuring the model: the Builder pattern
We designed our Connect $N$ game to have some flexibility in its parameters, so it’s worth discussing how a client can select those parameters when instantiating the model. We could, of course offer a constructor that takes all of the parameters that are allowed to vary:
/**
* Constructs a new game model with the given parameters.
*
* @param width the width of the grid (positive)
* @param height the height of the grid (positive)
* @param goal the goal line length for the game ({@code > 1})
* @param players the array of player names (non-null, non-empty, and
* each element non-null)
*/
public ConnectNModel(int width, int height, int goal, String[] players);
IllegalArgumentException
.)However, having to provide all four parameters every time is annoying, especially if standard Connect Four is the common case. It’s easy enough to provide a second, nullary constructor that uses the default values:
/**
* Constructs a new game model for Connect Four with the default parameters.
*/
public ConnectNModel();
However, what if want to change only one parameter—the width, say—while leaving the others at their defaults. With only the above two constructors, we must use the four-argument constructor, passing the width that we want and the default values for the other three parameters. That requires knowing the default values, and it’s tedious. Wouldn’t it be nice if there were a way to only specify how we want to differ from the defaults?
One common way to allow defaulting some of the parameters is to define
constructors with different sets of parameters. Unfortunately, this can
be confusing and error prone. For example, if there were a constructor
that took one argument, an int
, which game parameter would you
expect that to be?
Another common solution is to provide setters for the configuration parameters, so that the client can create a game model and then configure it by modifying it. However, this creates another problem: What happens if the client tries to reconfigure a game already in progress? Of course it could detect this and throw an exception, but this solution is not satisfactory because it violates two design principles: 1) that it’s better to make bad states or operations unexpressible than to catch them later, and 2) that when we want different behaviors, we should use different classes rather than a bunch of conditionals.
A better solution for handling several optional arguments is the builder pattern. When using the builder pattern, the client does not call the class’s constructor directly—in fact, we will make the constructor private, so that clients must instantiate model instances via the builder. Instead, the client first creates some kind of builder object, which encapsulates the configuration parameters for the game:
/**
* Constructs a builder for configuring and then creating a game model
* instance. Defaults to a standard game of Connect Four with players
* named "White" and "Red".
*
* @return the new builder
*/
public static Builder builder();
The builder starts out with the default parameter values, and then
provides several methods for changing whichever parameters we choose
without saying anything about the others. In this case, that means we
need a method for each of the parameters: the width, the height, the
goal, and the array of players. Then, the builder provides a method
build()
that instantiates a game model object using the
builder’s current parameters. Here’s the interface:
public static final class Builder {
/**
* Sets the width of the game grid.
*
* @param width the width (positive)
* @return {@code this}, for method chaining
*/
public Builder width(int width) { ... }
/**
* Sets the height of the game grid.
*
* @param height the height (positive)
* @return {@code this}, for method chaining
*/
public Builder height(int height) { ... }
/**
* Sets the goal line length for the game.
*
* @param goal the goal (positive)
* @return {@code this}, for method chaining
*/
public Builder goal(int goal) { ... }
/**
* Sets the players for the game. Makes a defensive copy of the
* player array, so the client cannot change it from under us.
*
* @param players the array of player names (non-null, non-empty,
* and every element non-null)
* @return {@code this}, for method chaining
*/
public Builder players(String... players) { ... }
/**
* Builds and returns the specified {@link ConnectNModel}.
*
* @return a new {@code ConnectNModel}
*/
public ConnectNModel build() { ... }
}
Then to create our wide Connect Four instance, we can use the builder object to specify only the parameters we want:
ConnectNGame.builder().width(15).build()
3.1 How to obtain a Builder
?
In the code above, we include a public static
method on the
ConnectNModel
class that provides a means of obtaining a Builder
object. We could just expose the constructor of the Builder
class
directly:
(new ConnectNGame.Builder())...
but this is less flexible and more tightly coupled than the previous
alternative. We’ve now forced our clients to depend on the Builder
class directly, but if our model evolves, and we eventually have multiple
subclasses of Builder
, our clients are forced to change code to match.
The static method, on the other hand, encapsulates the call to the
Builder
constructor, and so insulates the client from needing to know
how it works.
Additionally, we can now provide multiple such static methods, to
“preconfigure” the Builder
with some default options, or to select
among multiple builder implementations. This flexibility is not needed in this
simple example, but might come in handy later.
3.2 Committing to the Builder pattern
Once we decide that a Builder
makes sense, we ought to commit to it
fully. That entails making the constructor(s) of the ConnectNModel
class private
or protected
, so that clients cannot construct such
a model directly, but must instead go through the Builder
.
Why bother? Essentially, it boils down to a subjective decision on whether the
objects we’re trying to create are heavily configurable (now or in the
foreseeable future), or only mildly so. If there’s only mild configuration,
then perhaps a few well-chosen public
constructors will suffice. But if
we can foresee that there will be lots of independently customizable
parameters, then we can sidestep having to design lots of constructors, and at
the same time avoid letting customers begin to depend on details that we may
not want to expose later on. In short, the builder pattern lets us
future-proof our class against future customizability. It may not always be
necessary, but it’s often too late to introduce it afterward, so it’s worth
considering early whether it may be needed eventually.
Next time we will begin implementing the ConnectNGame
class.
1Tic-tac-toe has both rotational and reflective symmetries, which means that the orientation doesn’t actually matter so long as the UI is consistent.
2Static classes and enumerations are like static fields and methods, in that they are part of the class rather than part of each object of the class.
3Meaning that the client could do what this method does using the other public methods, but it’s still nice to have.