On this page:
11.1 The game of Connect-4
11.1.1 Analysis
11.1.2 Interface design
11.1.3 Instantiation
11.2 Building a complex class
11.2.1 The Builder class
11.2.2 Where does the Builder reside?
11.2.3 Mandating the builder as the only way to instantiate
11.3 Controller
11.3.1 Synchronous controllers
11.3.2 Asynchronous controllers
11.4 The role of the main method
6.3

Lecture 11: Building a model, builder pattern and controllers

Related files:
  code.zip  

11.1 The game of Connect-4

In Lecture 10: Design of programs, MVC architecture we built a model for the game of Tic-tac-toe. The model itself was a simple, as the game of tic-tac-toe is “fixed”. However a more complicated, customizable model presents some additional challenges.

The Connect-4 game is a popular strategy game that is played by 2 players. The game is played with a vertical board with 7 columns and a slot on its upper edge. Coins or discs can be inserted into the slot in a column, which will settle at the lowermost available position in the board. Each player uses a different-colored set of discs. The objective of the game is to get 4 discs of the player’s color in a line (horizontally, vertically or diagonally).

We can generalize this to a Connect-N game, where the objective is to have N discs in a line. We can further generalize the dimensions of the board, as well as the number of players. This creates a family of games with customizable board dimensions, number of players and goal.

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│
└─┴─┴─┘    └─┴─┴─┘    └─┴─┴─┘    └─┴─┴─┘
11.1.1 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 from Lecture 10: Design of programs, MVC architecture . We need ways to:

We also need to check for exceptional conditions, such as rejecting bad moves, playing out of turn, playing in a non-existent column, or playing in a column that is already full. We would also like to signal an error if the player asks for a winner before the game is over, or for the next player after the game is over.

11.1.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.

How to move?

To take a turn, it is sufficient to select which column the current player wants to play in. As there are many players (unlike tic-tac-toe) we design the move method to also accept the player who is making the move. The model can then verify that it is this player’s turn. 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);

Querying the configuration

The client must specify the game configuration (board dimensions, number of players and goal) when instantiating the model. However it is a good idea to let the client query these game configurations, without allowing to set them once the game has been created. 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();

Querying the game state

The game is always in one of three different statuses:

An enumerated type seems like a suitable choice to represent the state of the game. This does mean that our interface will be coupled with an extra non-primitive type. However we choose to use an enumeration because it accurately represents the state of the game. We can define an enumerated type as follows:
public enum Status { Playing, Stalemate, Won }
We then offer the getStatus method for a client to query the status of the game.

Finally, we also define a method to query whether the game is over.

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. We provide a method that allows the client to query the contents of a specific position on the board. Since we use player indices to refer to players, this method can return an integer. However how do we represent an empty slot?

We address this issue by using the wrapper class Integer as the return type of this method. As it is a reference type, null is a possible value which in this case will signal an empty cell. While null should be used sparingly, we use it here because its usage matches its purpose exactly: emptiness.

/** * 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);
11.1.3 Instantiation

Let us assume that we implement the above ConnectN interface in a ConnectNImpl class. As we have generalized the game, we would need to specify the dimensions of the board (width and height), the number of players and the goal. This can be easily done by offering a four-parameter constructor:

public class ConnectNImpl implements ConnectN {
...
public ConnectImpl(int width,int height,int goal,int numPlayers) {
...
}
}

What we if wanted to create the “default” Connect-4 game? For such a game, the width is 7, the height is 6, the goal is 4 and the number of players is 2. We can create such a game by using the above constructor as follows:

ConnectN gameModel = new ConnectImpl(7,6,4,2);

The above usage reveals a minor problem: when a constructor (or any method) takes multiple arguments of the same type, it is easy to switch the numbers (e.g. passing them in the order width, height, number of players and goal). The compiler will not catch such an error. It would be easier if we could create a “default” version of the game. This can be done by adding another constructor:

public class ConnectNImpl implements ConnectN {
...
public ConnectImpl(int width,int height,int goal,int numPlayers) {
...
}
 
public ConnectNImpl() {
this(7,6,4,2);
}
}

What if we wanted to create additional customized games, such as keeping the goal and number of players the same, but modifying the width and height? We can add more constructors to the class. Even with just four parameters to the model, the number of such constructors will quickly increase. However there is a bigger problem. We cannot simultaneously support two customizations: retaining the default width and height while modifying goal and players, and retaining the default goal and players while modifying width and height. This is because both such constructors would require two integer arguments, which is not allowed.

The above problems may seem minor in this example, but scale up quite easily, as shown in the next, unrelated example.

11.2 Building a complex class

Consider a class that represents a customer at the local pizzeria. This pizzeria accepts orders in person, over the phone or online. It provides carry-out, dine-in and delivery options. Therefore it maintains considerable information about its customers. This is represented by a Customer class. (Some of these attributes can be refactored into another class, but that does not change the problem we illustrate here)

public class Customer {
private String firstName;
private String lastName;
private String middleInitial;
private String addressLineOne;
private String addressLineTwo;
private String city;
private String state;
private int zipcode;
private String primaryPhone;
private String cellPhone;
private String userName; //online profile private double businessValue; //total amount earned from this customer private double percentageOnline; //percentage of amount earned by online order }

When a new customer record is created, one must instantiate the above class. The simplest way to do this is by offering a constructor in the above class.

public class Customer {
private String firstName;
private String lastName;
private String middleInitial;
...
 
public Customer(String firstName,
String lastName,
String middleInitial,
String addressLineOne,
String addressLineTwo,
String city,
String state,
int zipcode,
String primaryPhone,
String cellPhone,
String userName,
double businessValue,
double percentageOnline) {
this.firstName = firstName;
this.lastName = lastName;
...
}
}

This approach has many of the same problems as the ConnectNImpl example but at a larger scale:

  1. Using the constructor above is tedious and error-prone. For example, several of its parameters are String objects. It is likely that the user will pass the strings in the wrong order (pass last name before first name) and such errors will not be caught by the language.

  2. When a new customers walks into the pizzeria, he/she has no online presence. It is not necessary to record the customer’s phone number and address. This can be addressed by passing default values to the above constructor. The code that calls the constructor this way would seem arbitrary because it may not be obvious to the reader of the code that they are default values. Moreover such code may appear at several places in the program.

  3. There are many scenarios like the above (e.g. a customer who always places online orders but not use delivery does not need to have an address on record, a customer who places orders only on the phone does not have an online presence, etc.

One solution to the above problems may to be offer several constructors, each one expecting only a subset of the above values and assumes defaults for the rest. All such defaults would be in one place (in these constructors) and would be more manageable. With so many argument options, the number of constructors will quickly explode.

Such “large” or complex classes are not uncommon, and instantiating them creates problems that require a more scalable solution. It is desirable to somehow streamline the building process of such an object. In such situations we create a special class whose primary purpose is to facilitate building such objects.

11.2.1 The Builder class

The main idea of the builder is that it maintains copies of all the fields that would be required to build the complex object. It offers methods to edit each field individually, but maintains default values for all of them. A client would then choose to call as few or as many of these methods to set the values of these fields. In the end the client can call a method in the builder that would take the set fields, create the complex object and return it.

public class CustomerBuilder {
//maintains fields, one for each field in the actual complex class  
private String firstName;
private String lastName;
private String middleInitial;
private String addressLineOne;
private String addressLineTwo;
private String city;
private String state;
private int zipcode;
private String primaryPhone;
private String cellPhone;
private String userName; //online profile private double businessValue; //total amount earned from this customer private double percentageOnline; //percentage of amount earned by online order  
public CustomerBuilder() {
//assign default values to all the fields as above }
 
//call this method to change the first name public CustomerBuilder firstName(String firstName) {
this.firstName = firstName;
return this;
}
 
//call this method to change the zip code public CustomerBuilder zipcode(int zipcode) {
this.zipcode = zipcode;
return this;
}
 
...
 
public Customer build() {
//use the currently set values to create the Customer object return new Customer(firstName,lastName,middleInitial,...);
}
}

To create the record for a customer who only walks in:

Customer customer = new CustomerBuilder()
.firstName("Tom")
.lastName("Cruise")
.phoneNumber("1112223333")
.build();
 

To create the record for a customer who only orders online and has pizzas delivered:

Customer customer = new CustomerBuilder()
.firstName("Tom")
.lastName("Cruise")
.addressOne("123 Merry St")
.addressTwo("Northfield")
.city("Beverly Hills")
.zipcode(90210)
.build();
 

Thus the code to create the complex object is only as lengthy as it needs to be. Furthermore the code is clearer because the method names explicitly reveal which fields are being set.

Consider the way the above builder is used, specifically the line new CustomerBuilder().firstName("Tom").lastName("Cruise").... The only way this line would work is if the left of .lastName("Cruise") is a CustomerBuilder object. Supporting this terse syntax is the primary reason why each of these field-setting methods returns a CustomerBuilder object.

11.2.2 Where does the Builder reside?

Builders can be their own classes, as the above example shows. However the builder is often implemented in a way that closely couples it with the object that it builds.

A common way to implement builders is to write them inside the class they are building. The class to be built can provide a method that returns the builder used to build it.

public class Customer {
...
public static CustomerBuilder getBuilder() {
return new CustomerBuilder();
}
...
 
public static class CustomerBuilder {
...
 
private CustomerBuilder() {
...
}
}
 
}

This way there is only one way to create a builder: through the class it is building. The code to use the builder would change to:

Customer customer = Customer.getBuilder()
.firstName("Tom")
.lastName("Cruise")
.phoneNumber("1112223333")
.build();
 
11.2.3 Mandating the builder as the only way to instantiate

The builder pattern is effective, but now there are two ways to instantiate an object: directly through the constructor, or through the builder. We can mandate the latter and forbid the former by making the constructor of the outer class private.

public class Customer {
...
public static CustomerBuilder getBuilder() {
return new CustomerBuilder();
}
 
private Customer(...) {
...
}
...
 
public static class CustomerBuilder {
...
 
private CustomerBuilder() {
...
}
 
...
 
public Customer build() {
//use the currently set values to create the Customer object //with the only private constructor above. return new Customer(firstName,lastName,middleInitial,...);
}
}
 
}

This implementation is possible because inner classes can access the private methods and variables of the outer class.

Do Now!

Apply the builder pattern to the ConnectNImpl class.

11.3 Controller

The controller is the glue of the system. It takes input from the user and decides what to do. First it is the client of the model, and so it controls how and when the model is used. It also controls what must be shown by the view and when. Some programs put unique responsibilities on the controller depending on what they do.

11.3.1 Synchronous controllers

The game of tic-tac-toe has a predictable, linear progression. The program (i.e. the controller) must follow a fixed sequence of “get input, pass input to model, get board and pass to view” operations until a player wins or the game ends in a stalemate. In general this behavior is observed in applications that run according to certain rules, making the sequence of operations predictable. Controllers that implement such a pre-defined sequence are called synchronous controllers. They are common in games and programs that do batch-processing (e.g. converting all images in a folder to .jpg format, etc.).

The design of such a controller is typically a method that goes through this sequence in a loop. The loop may have branches depending on the application, but the sequence is predictable.

A challenge for such controllers is to abstract the user input. The function of the controller does not change if the input is sought from the keyboard, from a file, or another input source. The controller must be designed so that this independence is achieved as much as possible.

11.3.2 Asynchronous controllers

Many programs are reactive in nature. The program does not execute a pre-defined sequence of operations, but rather executes depending on user input. Most programs with graphical user interfaces behave this way. Asynchronous controllers are suitable for this purpose.

Instead of a single method that executes a sequence of operations, the functionality of an asynchronous controller is usually divided into several methods. Each method is called in response to specific user action.

11.4 The role of the main method

The division of a program into model, view and controller has implications on what the main method is supposed to do (in case of standalone programs). In this context, the main method should not directly implement any functionality (the model does this), should not directly interact with the user (the controller does this) and should not directly show any results (the view does this). The main method (or in general, the “starter” of the application) is reduced to creating the model, view(s) and controller(s) and then giving control to the controller.

class ProgramRunner {
public static void main(String[] args) {
Model model = new Model(...); //set up before if needed View view = new View(...);
//setup details of view if needed  
//create controller, give it the model and view Controller controller = new Controller(model,view,...);
 
//give control to the controller. Controller relinquishes only when program ends controller.go();
}
}