Concepts / Designing Classes with Private Instance Variables

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.

  • Programming

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?

  • Yes, if the object was rolled many times

  • No, each die must always show a value from 1 through 6

Reveal answer
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

Definition

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.

Calling codeoutside the classPairOfDice objectowns die1 and die21. construct: invariant must hold on finish2. constructor returns, die1 and die2 in 1 to 63. call roll(), invariant assumed on entry4. roll() updates dice, then re-checks invariant5. roll() returns, invariant holds again
In what order do the constructor and method calls run, and where is the invariant checked before and after each call?

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.

die1 = 3, die2 = 5valid statedie1 = 6, die2 = 1valid statedie1 = 7, die2 = 2forbidden, never reachedroll()
Which object states are allowed, and how does a method call move the object from one valid state to another without ever entering an invalid one?

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.

Outside codeother classes and mainPublic methodsroll and gettersdie1privatedie2privateInvariant: each die 1to 6enforced by class codecalls only through public methodsmay change, checkedmay change, checkedconstrainsconstrains
Which code can read or change the private values, and how must all outside access pass through public methods that enforce 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

java
Notes on the code
  • 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.
Worked example

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.

  1. 1
    Check 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.

  2. 2
    Check 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.

  3. 3
    Check 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.

  4. 4
    Check 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.

Result

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 like
    public int die1; in the class, then outside code writes d.die1 = 7;
    Why it fails

    Outside code can now change die1 directly, so the class can no longer check the change against the invariant.

    Do this instead

    Make 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 fails

    The 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 instead

    Check 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 like

    A method that sets die1 to whatever number the caller passes in, without checking the range.

    Why it fails

    The method must make sure the invariant still holds when it returns. Accepting any value lets the object enter a forbidden state.

    Do this instead

    Verify 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

Best practice

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.

MEDIUM

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.