Lecture 7: Encapsulation
1 Model representation
When we last talked about Connect $N$, we designed an interface for our game model:
interface ConnectNModel {
static enum Status { Playing, Stalemate, Won, }
Status getStatus();
boolean isGameOver();
int getNextPlayer();
int getWinner();
Integer getPlayerAt(int x, int y);
boolean isColumnFull(int which);
int move(int who, int where);
int getWidth();
int getHeight();
int getGoal();
int getPlayers();
}
The next step in implementing this interface is to consider what data we
need—Integer
s that will let us
Some of these methods return simple quantities that don’t change, so for those we can just store each in a field:
public int width;
public int height;
public int goal;
public int players;
Clearly we need to represent the state of the game grid, in particular which tokens are where. Since the grid is essentially a two-dimensional matrix, we can use an array of arrays or list of lists. In general this choice is arbitrary (except for the effects of locality), but in this case columns may make more sense, because we can use shorter lists to represent not-yet-full columns and grow them as necessary.
public List<List<Integer>> columns;
Finally, we need state in order to be able to tell the client who the current player is and who won. These fields, unlike the others, will be mutable:
public Status status;
public int turn;
1.1 Reductio...
Clearly the fields listed above will work, but what if we are thinking
about flexibility for the future? For example, perhaps we don’t want to
commit right now to players being represented as int
s, so we
generalize to Object
in the two places where we represented
players as integers:
public Object turn;
public List<List<Object>> columns;
Or perhaps we’re considering the possibility of extending Connect $N$ into a third dimension, or an arbitrary number of dimensions. If we want to represent $k$-D game grids, a list-of-lists won’t do, so perhaps it’s better to defer that decision until later as well. And of course, width and height are also insufficient for $k$-D, so we generalize with a map from dimension names to their sizes:
public Map<String, Integer> dimensions;
public Object hypercolumns;
At this point, we might notice that our game configuration consists of
the map dimensions
and two int
s, goal
and players
. We
could store those in the map as well, paving the way for adding more
properties in the future without having to change the representation. So
at this point, these are our fields:
public Map<String, Integer> configuration;
public Status status;
public Object turn;
public Object hypercolumns;
Now, not having played $k$-D Connect-$N$ before, I’m not sure that we won’t need more potential statuses in a game of that complexity, and for that matter, a turn may involve multiple players. But fear not! We don’t have to decide on either of those things now:
public Map<String, Object> properties;
public Object hypercolumns;
At this point, we might as well go big, right? We could represent the game model now, and every potential future game idea we might imagine, with one field:
public Map<String, Object> properties;
Now you’re programming in Python.
1.2 ...Ad Absurdum
With this change, what have we gained and what have we lost? Certainly we’ve gained a lot of flexibility, but in return we’ve replaced our expression of intent, the clear meaning of the several named fields, with an amorphous mapping. We’ve given up the ability to control the shape of our data.
Like almost any other property of a design, increasing flexibility involves trade-offs. The design we ended up with above is clearly too flexible, but is there reason to believe that the design we started with isn’t too general as well?
2 Bad freedoms
With increased flexibility comes additional ways to abuse that flexibility. In particular, there are a lot of things we can do with our initial representation that should likely be disallowed:
The
width
,height
,goal
, orplayers
fields might change mid-game.The
width
,height
,goal
, orplayers
fields might be zero or negative.The
status
orcolumns
field might benull
.The shape of the list-of-lists in
columns
might not match the dimensions inwidth
andheight
;columns.size()
might differ fromwidth
, or it may contain a column whose size exceedsheight
.Or
columns
might containInteger
values that don’t stand for players.And more generally, the client can look at or change whatever it pleases.
Some of the above bad things are easily prohibited using the correct language features, and others can be prohibited by careful programming.
2.1 Restricting fields using the language
This is the easy part. For fields whose values shouldn’t be
updated1Meaning the primitive or reference value in the fields; objects
referred to by references in final
fields can still be
mutated., we can tell the Java compiler using the final
keyword, and it will prevent the fields from changing for us:
public final int width;
public final int height;
public final int goal;
public final int players;
You might wonder, Why bother with final
when I can just not
change the fields? This question generalizes to any design choice that
imposes a restriction on how an object can be used, and the same answer
generally apply: People make mistakes. It could be you in six months
when you’ve forgotten how the class works, or it could be that your
coworkers and successors don’t know that the field isn’t supposed to be
changed. Sure, you could let them know with a comment, but comments are
easily missed and error messages aren’t. So just in case, arrange to get
that error message by using final
.
As a general rule, declare every field that you don’t intend to change
as final
.
The other problem that Java can solve for us directly is the last one, that clients have unrestricted freedom to access the class’s fields. We can lock clients out by specifying a more restrictive access level. Java has four, though one is implicit. Ordered from most to least restrictive:
Modifier |
| class |
| package |
| subclass |
| world |
| scope description |
|
| ✓ |
|
|
|
|
|
|
| same class only |
default |
| ✓ |
| ✓ |
|
|
|
|
| ... and everything else in the same package |
|
| ✓ |
| ✓ |
| ✓ |
|
|
| ... and subclasses |
|
| ✓ |
| ✓ |
| ✓ |
| ✓ |
| ... and the rest of the world |
The ordering is inclusive, in the sense that if a member is visible from
some other code with one of the modifiers, then it will also be visible
with the weaker modifiers (lower in the table). If a field, method,
constructor, or nested class, enumeration, or interface is marked
private
then it is visible only from within the same top-level
class. (That is, nested classes are considered to be part of the same
class for the purpose of access levels.) If the declaration is unmarked,
it has default or package scope, which means that is visible from
the entire Java package in which it lives. A protected
member
is additionally visible from any subclasses of the class where it’s
declared, and public
member is visible everywhere.
To see what this means in a bit more context, consider these four classes in two packages:
package first;
public class Base {
private int privateField;
int packageField;
protected int protectedField;
public int publicField;
}
class FirstHelper { ... }
package second;
public class Derived extends Base { ... }
class SecondHelper { ... }
From which classes is each member field of Base
visible? Just use the
table above!
Field |
|
|
|
|
|
|
|
|
|
| ✓ |
|
|
|
|
|
|
|
| ✓ |
| ✓ |
|
|
|
|
|
| ✓ |
| ✓ |
| ✓ |
|
|
|
| ✓ |
| ✓ |
| ✓ |
| ✓ |
As with final
, the best rule of thumb for using access level modifiers
is to follow the Principle of Least Privilege:
Every program and every privileged user of the system should operate using the least amount of privilege necessary to complete the job.
For fields, this means private
the vast majority of the time.
Exceptions are few:
Constants, meaning
static final
fields containing immutable values, are oftenpublic
.Fields that must be accessible to subclasses can be
protected
(though often it’s better to give a subclass that kind of access via methods).When multiple classes within the same package are cooperating in some close way such that it doesn’t make sense for them to communicate via interfaces, you can use the default access level.
Every time you make a field more accessible than it needs to be, you lose further control of what happens to it, and some ability to change that part of the representation in the future. In the next lecture, we’ll explore how we can use the control that access levels give us in order to eliminate additional bad freedoms.
1Meaning the primitive or reference value in the fields; objects
referred to by references in final
fields can still be
mutated.