Designing Classes with Private Instance Variables
A class invariant is a statement that must always be true about objects of a class, such as each die in a PairOfDice object showing a value from 1 through 6.
A Pair of Dice That Must Never Show a 7
Imagine you are designing a class that models a pair of dice. You create a PairOfDice object, roll it many times, and read its values. Somewhere in your design you have a fact you are counting on without even writing it down: each die shows a number from 1 through 6. You never expect a die to show 7, and if one ever did, the rest of your program would be built on a false assumption.
What do you think happens?
In a PairOfDice object, can one of the dice ever be allowed to hold the value 7?
Reveal answer
No. Each die must show a value from 1 through 6.
The source states that when you design a class you expect certain facts about its objects to hold no matter what happens, and the example given is that each die in a pair of dice shows a number from 1 to 6. This fact is supposed to be true about every object of the class at every moment of its life.
The fact that the dice always show 1 through 6 is not a passing observation. It is a rule about the objects themselves. This article explains how programmers name such a rule, why it depends on how instance variables are declared, and how it shapes the methods you write.
What a Class Invariant Says
A class invariant is a statement that must always be true about the objects of a class. It is supposed to hold for every object of the class, at every moment in that object's life.
In the PairOfDice class used in the source, the values shown on the dice are stored in instance variables named die1 and die2. A natural class invariant for this class is that the values of die1 and die2 are in the range 1 through 6. This statement describes any pair of dice created from the class. It is a property of objects, not a property of the class definition as such. The class definition is just the text that describes how objects are built; the invariant is a fact that every object built from that text must satisfy.
Notice the difference between a fact you check once and an invariant. A check made once, such as printing a die value to see whether it looks right, tells you about one moment. An invariant is a promise that covers all moments: after construction, after every method call, and in between. That is why the invariant has to be designed into the class rather than tested occasionally from the outside.
Tracing the Invariant Through an Object's Life
The source gives a precise rule for where the invariant must be true. A class invariant is a postcondition of every constructor. A postcondition is a statement that must hold when code finishes, so when a constructor returns, the invariant must already hold for the new object. The source also says that a class invariant is both a precondition and a postcondition of every method in the class. A precondition is something the code may assume is true when it starts, and a postcondition is something the code must make true when it finishes.
Read the diagram from top to bottom. The constructor is where the invariant first has to be established, because it is a postcondition: when the object is handed back to the caller, both dice must already be in the range 1 through 6. Each later method call begins with the invariant assumed to be true, which is the precondition side of the rule, and ends with the method having to restore it, which is the postcondition side. The method does not have to be clever about what state the object was in before; it can rely on the invariant as its starting point and must return the object to a valid state.
Valid States and Forbidden States
Think of the invariant as dividing all possible states of a PairOfDice object into two groups. The valid states are the ones where both die values lie between 1 and 6. The forbidden states are the ones where either value falls outside that range. The invariant promises that the object is never in a forbidden state. Method calls are the only events that move the object from one state to another, so the design question is whether every method can move the object only between valid states.
The diagram shows one valid roll. The object starts with die1 at 3 and die2 at 5, and a call to roll() moves it to die1 at 6 and die2 at 1. Both end states are valid. The third state, with die1 at 7, is not a state the class is allowed to produce, so no method should ever lead to it. The invariant is not a rule that the caller remembers to obey; it is a rule that every method in the class is responsible for keeping true.
Why the Dice Values Must Be Private
A class invariant can only be guaranteed if the class controls every way the relevant variables can change. This is why the source makes die1 and die2 private. A private instance variable is one that is not directly accessible from outside the class. Its value can only be changed by code inside the class, so the class author can check every change against the invariant.
Making the variables private is therefore not only a matter of style. It is what makes the invariant enforceable. The source describes the consequence directly: once die1 and die2 are private, the programmer only has to make sure that all of the code in the class definition respects the class invariant. Outside code cannot break it, because outside code has no direct access to the values. Every route to changing a die passes through code the class author wrote and can inspect.
A Class That Protects Its Dice
- The constructor sets both dice to 1, so the invariant holds when construction finishes.
roll()produces each die value in the range 1 through 6, so it restores the invariant on exit.- Only getters are provided, so outside code can read the values but cannot write them directly.
Checking the invariant for a constructor and a method
Show that the PairOfDice class above keeps the invariant that die1 and die2 lie in the range 1 through 6, both when an object is created and when roll() is called.
- 1Check the constructor as a postcondition
The constructor assigns die1 = 1 and die2 = 1. Both values are in the range 1 through 6, so the invariant holds when the constructor finishes.
- 2Check the method's precondition
When
roll()is called, the invariant is assumed to hold. The code does not need to repair a bad starting state, because the object was only ever in valid states. - 3Check the method's postcondition
Math.random()* 6 gives a value from 0 up to but not including 6. Casting to int gives 0 through 5, and adding 1 gives 1 through 6. Both assignments therefore put the dice back in range. - 4Check that outside code cannot break it
die1 and die2 are private and have no setters. The only code that writes them is the constructor and
roll(), so the invariant depends only on code inside the class.
The invariant holds after construction and after every call to roll(), and no outside code can violate it, so every PairOfDice object stays in a valid state.
Mistakes That Break the Invariant
Common mistake Making the dice values public
What it looks likepublic int die1; in the class, then outside code writes d.die1 = 7;Why it failsOutside code can now change die1 directly, so the class can no longer check the change against the invariant.
Do this insteadMake die1 and die2 private so that every change goes through code inside the class.
Common mistake Assuming the invariant only needs to hold after the program finishes
Why it failsThe source states that the invariant must hold at every moment in an object's life, including after construction and after every method call.
Do this insteadCheck the invariant as a postcondition of each constructor and as both a precondition and a postcondition of each method.
Common mistake Writing a method that trusts its caller to supply valid values
What it looks likeA method that sets die1 to whatever number the caller passes in, without checking the range.
Why it failsThe method must make sure the invariant still holds when it returns. Accepting any value lets the object enter a forbidden state.
Do this insteadVerify that the new values are in the range 1 through 6 before storing them, or compute values that are always in range.
Common mistake Treating the invariant as a comment instead of a design rule
Designing a Class Around Its Invariant
Begin the design by stating the class invariant in plain words, such as each die shows a value from 1 through 6. Then make every instance variable that the invariant covers private, provide only the public methods you need, and write each constructor and method so that it establishes or preserves the invariant.
A colleague writes a PairOfDice class with public instance variables die1 and die2 and a method that lets the caller set any value. Explain which part of the design makes the invariant impossible to guarantee, and describe how you would change the class so that the invariant can be enforced.
Hints
- Ask who is allowed to change die1 and die2 in this design.
- Think about whether the class can check a value before it is stored when the caller writes the variable directly.
- Consider how the constructor and
roll()would need to change for the invariant to be a postcondition of each.
Key Takeaways
- A class invariant is a statement that must always be true about every object of a class, such as each die in a PairOfDice showing a value from 1 through 6.
- A class invariant is a postcondition of every constructor, so it must hold for a new object when construction finishes.
- A class invariant is both a precondition and a postcondition of every method: a method may assume it holds on entry and must make sure it still holds when it returns.
- Instance variables covered by an invariant should be private, so that all changes go through code in the class that can enforce the invariant and outside code cannot break it.