On this page:
15.1 The game of Connect-N:   revisited
15.1.1 Model Representation
15.1.2 Encapsulation and flexibility
15.1.2.1 Representing turn
15.1.2.2 Generalizing dimension
15.1.2.3 Game configurations
15.1.2.4 Properties
15.1.3 Flexibility:   is it such a good thing?
15.1.4 Encapsulation and specificity
15.1.4.1 Restricting access and change
15.1.4.2 More general restrictions
15.2 Class invariants
15.3 Invariants
15.3.1 Examples
15.3.2 Other invariants
6.3

Lecture 15: Encapsulation and invariants

15.1 The game of Connect-N: revisited

In Lecture 11: Building a model, builder pattern and controllers we created a model for the game of Connect-N. For reference, here is the interface that we designed:

 
public interface ConnectN {
 
/** * 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);
 
 
int getWidth();
 
int getHeight();
 
int getGoal();
 
String[] getPlayers();
 
Status getStatus();
 
/** * Determines whether the game is over. * * @return whether the game is over */
boolean isGameOver();
 
/** * 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();
 
/** * 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);
}
15.1.1 Model Representation

In Lecture 11: Building a model, builder pattern and controllers we did not elaborate on the implementation of this interface, beyond issues related to instantiation.

When we implement the above model we must decide how to represent the data (the fields). Intuitively we need some way to represent the game grid and its tokens. We also need to represent the game configuration parameters (width, height of grid, goal and number of players) and status of the game (whose turn it is, what is the current status of the game).

Using this information we arrive at the following representation:

public class ConnectNImpl implements ConnectN {
//game configuration private int width;
private int height;
private int goal;
private int players;
 
//status of the game private Status status;
private int turn;
 
//grid private List<List<Integer>> columns;
}
15.1.2 Encapsulation and flexibility

The ConnectNImpl class encapsulates all the data and operations required to emulate the model of a game of Connect-N. Recall that good encapsulation promotes high cohesion, which leads to better protection from variation.

Since we wish to guard the design from extensive changes in future, we may want to make the representation flexible (this was the reason we generalized to Connect-N, so that a larger game of arbitrary complexity can be represented by our model without any changes). So can we make the above design even more flexible?

15.1.2.1 Representing turn

By using private int turn to represent whose turn it is, we confine ourselves to representing players as integral numbers. While this may have some computing advantages (e.g. determining whose turn it is next) it is not the most intuitive representation. For example players may want to name themselves (like in a bowling alley). But that would mean committing to another specific type: String. We can be non-specific, by making it be any valid data type in Java by using private Object turn! This would also change the representation of the grid to List<List<Object>> columns.

15.1.2.2 Generalizing dimension

We generalized from Connect-4 to Connect-N, but kept the grid 2-dimensional. Can we imagine a 3D version of this game? It would surely be much easier to emulate on a computer than using physical objects! How about an arbitrary dimension–k-D Connect N?

This and other generalizations may appear a bit unrealistic at first. But a lot of designs are not generalized to include a scenario precisely because the scenario is not anticipated in advance.

Changing dimensionality seems to be more complicated. To make a 3D version of Connect-N, the grid needs to be represented as List<List<List<Object>>> columns. To make a 4D version, it would be List<List<List<List<Object>>>> columns, and so on. How would we represent an n-dimensional list, without knowing what n is?

We need a type can be used to refer to any of these “hyper lists”. We can use...Object again, because it can refer to any of them!

Generalizing to k dimensions changes a lot about the game. Can we be sure that the status of the game can now be one of “Playing”, “Stalemate” and “Won”? What else could happen? More importantly how would we store status in general for a game of arbitrary dimension? We can use Object again!

Our modified representation is now:

public class ConnectNImpl implements ConnectN {
//game configuration private int width;
private int height;
private int goal;
private int players;
 
//status of the game private Object status;
private Object turn;
 
//grid private Object hyperColumns;
}
15.1.2.3 Game configurations

If the game is k-dimensions, the game configuration should be able to represent each of these dimensions (instead of simply width and height). Currently we have a different integer variable for each dimension, named after the dimension. However when we generalize to k-dimensions, it is difficult to explicitly enumerate each dimension this way. We could use a list to store all the dimensions, but there was some value in naming each dimension (as the existing variables are). We can achieve both by storing (name,numeric-value) pairs in a map:

public class ConnectNImpl implements ConnectN {
//game configuration private Map<String,Integer> configuration;
private int goal;
private int players;
 
//status of the game private Object status;
private Object turn;
 
//grid private Object hyperColumns;
}

Since the goal and players are also integers, we could just add them to the map. The map now represents “properties” instead of “configuration”.

public class ConnectNImpl implements ConnectN {
//game configuration private Map<String,Integer> properties;
 
//status of the game private Object status;
private Object turn;
 
//grid private Object hyperColumns;
}
15.1.2.4 Properties

We note that status and turn also represent dynamic properties of the game. Since we have a map to store properties, can we use it to store them too? We could, if we imagine them as having names “status” and “turn”and generalize the map itself to store any type of values.

public class ConnectNImpl implements ConnectN {
//game configuration private Map<String,Object> properties;
 
//grid private Object hyperColumns;
}

And finally since we have this all-encompassing map, we can absorb the grid into it as well. This creates the following (final) representation:

public class ConnectNImpl implements ConnectN {
//game configuration private Map<String,Object> properties;
}

Targeting flexibility in anticipation of future enhancements may lead to unproductive design. The anticipated future enhancements may never materialize, or may be different than what were anticipated. The potential value added by the flexibility is mitigated by the resulting unnecessary complexity and reduced readability. Flexibility should be an important consideration in design, but in smaller steps. More importantly one should be open to refactoring design and code to reduce problems arising from unnecessary flexibility. Both are essential elements in many lean software development processes, such as extreme programming.

15.1.3 Flexibility: is it such a good thing?

The undeniable advantage of the above representation is that it successfully represents a k-dimensional Connect-N model in the most succinct manner possible. However we can also see that it has lost all contextual meaning and intent. More specifically:

  1. By looking at this model one cannot tell that it represents a game of Connect-N. It can represent anything!

  2. The representation does not guarantee any of these properties actually exist in the map. What if some entries were missing?

  3. The representation allows any property to be represented using any type. It does not take advantage of any type-checking capabilities

This exercise shows that focusing singularly on flexibility leads to a context-less and absurd design. Even if we do not go to this extreme, focusing on flexibility and generality often produces a design that is difficult to understand, and one that has difficulty enforcing specific constraints (the loss of type checks above, for example).

15.1.4 Encapsulation and specificity

How about the other way? Can our initial representation be too general?

public class ConnectNImpl implements ConnectN {
//game configuration private int width;
private int height;
private int goal;
private int players;
 
//status of the game private Status status;
private int turn;
 
//grid private List<List<Integer>> columns;
}
15.1.4.1 Restricting access and change

By using private we have used information hiding to restrict access to these variables. A byproduct of this is that we are assured that these variables cannot be changed from anywhere outside this object. But what about methods within this object?

It is a reasonable assumption that a model object, once created, represents a particular game of Connect-N. This means that its width, height, goal and number of players do not change. We can ensure this by making these variables final.

public class ConnectNImpl implements ConnectN {
//game configuration private final int width;
private final int height;
private final int goal;
private final int players;
 
//status of the game private Status status;
private int turn;
 
//grid private List<List<Integer>> columns;
}
15.1.4.2 More general restrictions

We want other restrictions on some of these variables:
  • The grid dimensions, goal and number of players cannot be negative

  • The dimensions of the list match the grid dimensions

  • The turn should always represent a valid player

The first two can be enforced more easily, since they can be dealt with mostly in the constructors. The third one seems more challenging, because turn represents a part of the state of this object that keeps changing throughout the life of the object. How can we ensure such restrictions are enforced? Taking a step back, how would we even anticipate such restrictions at the time of design?

15.2 Class invariants

Consider a class whose objects represent even numbers.

/** * An even integer. */
final class Even {
/* * Constructs an {@code Even} from an even {@code int}. * * @param value the {@code int} representation of the even number * @throws IllegalArgumentException if {@code value} isn't even */
public Even(int value) {
if (value % 2 != 0) {
throw IllegalArgumentException("value must be even");
}
 
this.value = value;
}
 
/** * Returns the even value as an {@code int}. * * @return the even {@code int} */
public int getValue() {
return value;
}
private final int value;
}

How do we know that an object of this class always represents an even number? We make the instance variable final which means it may not mutate after initialization. Then the constructor rejects any initial value that is not even. We make the class final so that it is not possible to extend it and break this rule by overriding.

Consider a related class, whose object represents a counter that counts only even numbers.

final class EvenCounter {
public EvenCounter(int value) {
if (value % 2 != 0) {
throw IllegalArgumentException("value must be even");
}
 
this.value = value;
}
 
public int nextValue() {
return value += 2;
}
private int value;
}

Since the object will count, the instance variable value cannot be final. So in addition to ensuring evenness in the constructor, we must ensure that any mutation retains this evenness. Making the variable private ensures that such mutations cannot happen directly from outside this class. The only other method that mutates it is nextValue which ensure evenness (why?).

Do Now!

Would public void halve() { value /=2; } violate this restriction? How about public int half() { return value/2;}

15.3 Invariants

When we design something we must pro-actively identify such restrictions, so that when we implement it we know what to enforce and avoid. Such rules are called invariants. There are many kinds of invariants, categorized over the scope over which they hold (invariants about a single variable, invariants that hold over a class, invariants that hold over a group of closely-related classes, etc.).

A class invariant is a logical statement about the instantaneous state of an object (of that class), that can be enforced by the constructors and ensured by its methods.

When we design a class we should think about what an object of that class represents, and then see if certain properties should hold at any time for such an object. We can then express this as a class invariant. Note the following characteristics of a class invariant:

The last part of the above definition tells us how to implement the class such that the invariant actually holds for its objects. Thus a class invariant allows us to express a restriction in design, and then faithfully implement it.

15.3.1 Examples

For the above EvenCounter example, the following are possible but incorrect class invariants:

  1. value is small: Not an invariant because it is not a logical statement

  2. value does not change: Not an invariant because it is not instantaneous

  3. value increases only by 2 at a time: Not an invariant, because it is not instantaneous

  4. value is an integer: An invariant, but not very useful if implementing in Java

  5. value is even: An invariant, and useful too!

In general if a statement X is proposed as a class invariant here is how to verify that it is so:

  1. Is it logical (answerable in yes or no)? If not, reject it

  2. Is it about the state of an object? If not, reject it

  3. Is it instantaneous (If you pre-pend the phrase “At any point in time” to X, does it make sense? If not, reject it

Some other designs that we have already seen also reveal invariants. As we list them here, think about how we implicitly knew them, and ensured that our implementations held them. Note that not all of them are class invariants, but invariants nevertheless.

  1. A list ends with an empty node.(Lecture 6) We imposed this invariant in all methods that mutated the list. We used the enforcement of this invariant in most of the list operations (this was critical to our recursive implementations).

  2. Either an employee is a CEO, or has a single supervisor.(Lecture 8) We imposed this invariant by carefully implementing the addSupervisee method. We used the enforcement of this invariant in many of our hierarchy operations.

  3. A duration is positive.(Lecture 3) We imposed this invariant by making instance variables final and checking this in the constructor.

Do Now!

Find any invariants for the Connect-N model. Revisit prior assignments and labs and think about any relevant invariants. Would explicitly stating them before you implement them help?

15.3.2 Other invariants

Some invariants may be logical. For example, a binary search tree has the structural property that for any node all items lesser than that in the node are in its left subtree and those greater than it are in its right subtree. Knowing this invariant is important because all the methods in the tree class must enforce this for correctness.

Some invariants are structural. For example a balanced binary search tree is one where each node is balanced. This ensures that operations on it are efficient. As another example, some hash tables need to be at least half-empty for them to be efficient.

Identifying invariants during design helps us to ensure correct implementation. It also helps us to evaluate the correctness of an existing implementation. In fact most of the unit tests we write verify that a given implementation obeys some specifications, which are in many cases, invariants of some kind!