Class Invariants
CS 3500, Fall 2015
Bad freedoms
What fields does a Connect $N$ model need?
Map<String, Object> properties;
What might be wrong?
might not have
width
,height
,goal
,columns
, etc.those properties might have the wrong types
The language lets us rule these possibilities out
Connect $N$ model fields, take two
int width;
int height;
int goal;
int players;
Status status;
int turn;
List<List<Integer>> columns;
What might be wrong now?
width
orheight
orgoal
orplayers
might change mid-gamewidth
orheight
orgoal
orplayers
might be zero or negativestatus
orcolumns
might benull
the shape of the list-of-lists in
columns
might not match the dimensions inwidth
andheight
or it might contain
Integer
s that don’t stand for actual playersand the client can look at or change whatever it pleases
The language lets us rule some of these possibilities out
Internalize immutability constraints using final
final int width;
final int height;
final int goal;
final int players;
Status status;
int turn;
final List<List<Integer>> columns;
What might be wrong now?
width
orheight
orgoal
orplayers
might change mid-gamewidth
orheight
orgoal
orplayers
might be zero or negativestatus
orcolumns
might benull
the shape of the list-of-lists in
columns
might not match the dimensions inwidth
andheight
or it might contain
Integer
s that don’t stand for actual playersand the client can look at or change whatever it pleases
Ensure correct initialization using a constructor
public ConnectNModelImpl(int width, int height,
int goal, int players)
{
if (width < 1 || height < 1 || goal < 2 || players < 1) {
throw new IllegalArgumentException(…);
}
columns = new ArrayList<>(width);
for (int i = 0; i < width; ++i) {
columns.add(new ArrayList<>(height));
}
...
}
(If the constructor is private, checking might happen in a factory.)
What might be wrong now?
width
orheight
orgoal
orplayers
might change mid-gamewidth
orheight
orgoal
orplayers
might be zero or negativestatus
orcolumns
might benull
the shape of the list-of-lists in
columns
might not match the dimensions inwidth
andheight
or it might contain
Integer
s that don’t stand for actual playersand the client can look at or change whatever it pleases
The last point is key!
Gain control by using private
private final int width;
private final int height;
private final int goal;
private final int players;
private Status status;
private int turn;
private List<List<Integer>> columns;
Access levels
Access level modifiers
Modifier | Scope |
---|---|
private | same class only |
default | ...and everything else in the package |
protected | ...and subclasses |
public | ...and the rest of the world |
Where can it be seen from?
Modifier | class | package | subclass | world |
---|---|---|---|---|
private | ✓ | ✗ | ✗ | ✗ |
default | ✓ | ✓ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ |
public | ✓ | ✓ | ✓ | ✓ |
Who can see members of Base
?
package left; package right;
public class Base { ... } public class Derived
extends Base { ... }
public class lefthelper { ... } public class righthelper { ... }
Modifier | Base | LeftHelper | Derived | RightHelper |
---|---|---|---|---|
private | ✓ | ✗ | ✗ | ✗ |
default | ✓ | ✓ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ |
public | ✓ | ✓ | ✓ | ✓ |
An example
final class Even {
public Even(int value) {
if (value % 2 != 0) {
throw IllegalArgumentException("value must be even");
}
this.value = value;
}
public int getValue() {
return value;
}
private final int value;
}
How do we know that value
is always even?
The constructor ensures it
It can't change
Another example
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;
}
How do we know that value
is always even?
The constructor ensures it
Only methods can change it
Method
nextValue()
preserves evennessThere aren't any other (public) methods
Ruling out nonsense
Which methods are okay?
public void reset() {
value = 0;
}
public int scale(int factor) {
return value *= factor;
}
public int halve() {
return value /= 2;
}
public int half() {
return value / 2;
}
What’s going on here?
A class invariant is a logical statement about the instantaneous state of an object that is ensured by the constructors and preserved by the methods.
public int nextValue() {
return value += 2;
}
private int value;
// INVARIANT: value is even
Definitions
A class invariant is a logical statement about the instantaneous state of an object that is ensured by the constructors and preserved by the methods.
logical statement: a claim that is true or false
the instantaneous state of an object: meaning the values in its fields at a point in time
ensured by the constructors: when a constructor finishes normally then the claim is true
preserved by the methods: if the claim is true before a method is called then it is true afterward
Non-examples
A class invariant is a logical statement about the instantaneous state of an object that is ensured by the constructors and preserved by the methods.
// INVARIANT: value is small
Not a logical statement—too vague
// INVARIANT: value never decreases
Not instantaneous—not even wrong!
// INVARIANT: value is non-negative
Just not true
// INVARIANT: value is an int
True, but vacuous because Java guarantees it
Possible examples from Connect $N$
private final int width;
// INVARIANT (1): width is not null
// INVARIANT (2): width > 0
// INVARIANT (3): width > height
// INVARIANT (4): width never changes
private int turn;
// INVARIANT (5): turn only increases
// INVARIANT (6): turn > 0
// INVARIANT (7): turn < players
(For those reading along at home, only 2 and 7 are invariants.)
More examples from Connect $N$
private List<List<Integer>> columns;
// INVARIANT (1): columns != null
// INVARIANT (2): columns.size() == width
// INVARIANT (3): columns.get(col) != null if 0 <= col < width
// INVARIANT (4): every column in columns has size <= height
// INVARIANT (5): every Integer in columns is a valid player
// in (0, players]
// NOT AN INVARIANT: columns
// NOT AN INVARIANT: columns agrees with width
// NOT AN INVARIANT: columns always refers to the same list
// INVARIANT BUT VACUOUS: columns is a list
Class invariants enable rely-guarantee reasoning
The class invariant reasoning principle:
If the constructor ensures it,
and the methods preserve it,
then the methods can rely on it.
Rule out representations you don't want (whether invalid or merely inconvenient)
Example: rational numbers
“Merely inconvenient” representation:
public Rational(long numerator, long denominator) {
if (denominator == 0) {
throw new IllegalArgumentException(DEN_ZERO_MSG);
}
num = numerator;
den = denominator;
}
private final long num;
private final long den;
// INVARIANT: den != 0
Continue with the code.