You will want, as part of your design, to document the invariant properties of every class in your program. The invariants are those aspects of your class which must be true of any object for that object to be a valid instance of the class. For example, an Employee
object might define its invariants as follows:
Employee
has a nameEmployee
has a social security numberEmployee
has a level between 1 and 99Employee
earns between $23,000 and $100,000,000Employee
's salary must be within band
The definition of "within band" might be defined such that level 3 employees always earn between $32,000 and $67,500, level 4 employees earn between $45,000 and $98,000 and so forth. If the issues are sufficiently complex, you might encapsulate these policies in an EmployeeLevel
object, and this invariant of Employee
would then become "Every Employee
must have a valid EmployeeLevel
."
Careful definition of the invariants is critical to designing robust objects, and these invariants should be captured in a method of the class. Each class you design should have an Invariants()
member method, which returns TRUE
if all of the invariants are true, for example:
bool Employee::Invariants()
{
return
(
( myName.Length() > 0 ) &&
( mySocialSecurityNumber.IsValid() ) &&
( myEmployeeLevel.IsValid())
);
}
This simple method can be called to test whether the Employee
's myName
variable has content, whether the Employee
's social security number is valid (defined by the mySocialSecurityNumber
object itself) and also whether the EmployeeLevel
object is valid (again defined by that object itself). Tests of greater or lesser complexity can be encapsulated in this single method.
Each remaining method of Employee
can then assert that the object is in a valid state at the beginning and end of each method call. For example:
bool Employee::SomeMethod()
{
Assert(Invariants());
// do some work here
Assert(Invariants());
}
This idiom causes SomeMethod()
to assert that the object is in a valid state, both when the method begins and after the method completes its work. It is perfectly legal for a method to move an object into an invalid state during the course of the method, so long as the object is valid when the method completes.
Thus, if a method might change the employee's level and then update his salary, it is possible that in the middle of this method, the object would have an invalid EmployeeLevel
object. This is fine, so long as the object is returned to a valid state before the method returns.
Note that you are using ASSERT()
with Invariants()
. This indicates that you consider it to be a bug if Invariants()
returns FALSE
. This is a reflection of the design of the class — the invariants represent only those things that must be true for the object to be valid.
The premise that an object can be temporarily invalid becomes somewhat more complex in the presence of multithreading, as it may be that an object cannot tolerate being invalid when the thread loses control. In this case, you should introduce synchronization mechanisms (discussed in Chapter 5) to protect the object from being accessed by other objects, until it is returned to a valid state.
There are two methods which cannot, by definition, bracket their work with an assertion that the object is valid:
The constructor's job is to create a valid object; until its work is done, there is little point in asserting that the object is correct. Thus, you should ASSERT()
the invariants only at the conclusion of the constructor. Similarly, while you can (and should) ASSERT()
that the object is valid at the start of the destructor, by definition, the object will no longer be valid when the destructor completes.
As your class evolves, it is critical that you update your Invariants()
method to reflect the definition of a valid object. Careful use of the Invariants()
method and religiously asserting their validity at the start and conclusion of every member method can root out and identify bugs early in the development process.