Guidelines for Class Design, Part 2
More on Encapsulation
Immutable classes
- Recall that a class builds only immutable objects if there are
no mutator functions in the class
- i.e. the only functions that can set or change instance variables
(the member data) are the constructors of the class
- In Java, the String class is a good example
- This is a more important concept in a language like Java than in
C++
- In C++, appropriate applications of const can more finely
control what data can be changed and where
- If a Java class is immutable -- then objects, once created, cannot
change. So multiple references to the same object are not dangerous.
- Remember, in Java all class variables are reference
variables, which can attach to the dynamically created objects
- Also remember that in Java, garbage collection is automatic. There's
no delete operator to do it manually
- If a Java class is NOT immutable, then multiple references to the same
object could be problematic
- Specifically, avoid returning a reference to an internally stored
object from a class method, else an outside caller could change
it
- Similarly, avoid initializing an internal reference to attach
directly to an external object (passed in to the class as a
parameter, for example)
- Make or return copies (clone the object) instead
Unintended side effects
- A true accessor doesn't change the calling object, and a mutator does.
But if a method changes something else, it could be an unintended
side effect. For example:
- Changing an explicit parameter
- Changing a public static field
- Changing a global or more broadly used variable (like
cout) in C++
- Consider this C++ function, an operator overload as a friend of a
Money class, which stores a monetary value in integer dollars and cents:
ostream& operator<< (ostream& os, const Money& m)
{
double value = m.dollars + (m.cents / 100.0);
os << fixed << setprecision(2) << "$ " << value;
return os;
}
- See any side effects here? No Money object is changing, but what
about the stream object itself?
- The stream object (os) has two stream manipulators applied
here: fixed and setprecision(2)
- These manipulators remain in effect until changed -- for all future
outputs with this stream
- Not only has the function printed the monetary value, it has also
altered all future output formatting for the caller (without
their knowledge!)
- Another example (Java). What if we wrote Fraction multiply this way?
public Fraction multiply (Fraction f)
{
f.set(num * f.num, denom * f.denom);
return f;
}
Would the function return the correct result? Yes. But we've changed the
parameter in the process, and f is not a copy, just a reference to the
original! This was proably not intended!
- In C++, we can protect some things against unintended change through
appropriate usage of const -- the compiler will double-check
- Beware of unintended side effects when coding class methods!
Contracts
- Contracts are constraints on a class that allow builder and
users to share the same assumptions about the class
- Three primary types of constraints:
- Invariant: Something that is true for all instances of a
class.
- Precondition: Something that must be true before an operation
is invoked (to guarantee that it will work correctly). Users need
should meet such conditions before calling an operation
- Postcondition: Something that must be true after an operation
is invoked. Condition is to be met by the implementor.
- OCL -- Object Constraint Language
- A notation for formalizing constraints on elements of a model (like
obbjects, methods, etc)
- A constraint is expressed in OCL as a boolean expression
- Constraints can be expressed in OCL and added to a UML diagram,
depicted with notes attached to UML diagram elements
- However, too many notes on diagrams leads to clutter. OCL
expressions can be written in a text form, too
Invariants
Invariants are associated with a class. To prove that an invariant is
true, you must check:
- that it is true after each constructor of the class has run
- that it is preserved by every mutator
Examples
- In a Fraction class, the instance variable
denominator is always positive
- In a Blackjack class (implementing the game),
numPlayers > 0 is always true
- In a Date class, (1 <= month && month <= 12)
Preconditions and Postconditions
Preconditions and postconditions are associated with specific methods of a
class.
- The class user must be able to check the precondition of a method (so
the precondition cannot involve constraints on anything private -- the
user doesn't have access to private)
- Postconditions must be satisfied by the implementation of a
method
Examples:
- Consider a class called Table, which stores student
information, and consider a method with this prototype:
void Insert(int ssn, Student s);
Precondition: The ssn is not currently in the table
Postcondition: The student is now stored in the table, and the
specified SSN can be used to look up the student's record in later
operations
- For a Temperature class, consider this method:
void Convert(char newscale);
// we assume that before this call, an object already represents a valid
// temperature -- call it T
Precondition: newscale is one of these: 'c', 'C', 'f',
'F', 'k', 'K' (representing celsius, fahrenheit, or kelvin)
Postcondition: The temperature has been converted to the scale
represented by newscale, equivalent to temperature T
- For a LinkedList class, consider:
void Delete(int position); // deletes the item at the given position number
Precondition: size() > 0 (i.e. there must be items in the
list)
Precondition: size() <= position (i.e. there must be at
least position items in the list)
Postcondition: size() >= 0
Postcondition: Element at location position has been
removed from the list
OCL Expressions
To express constraints on a UML diagram with notes, use steroetypes,
along with the boolean expressions:
<<invariant>>
denominator > 0
<<precondition>>
size() > 0
<<postcondition>>
size() < position
To express constraints in text form in OCL, use keywords context,
along with inv, pre, or post:
context Fraction inv:
GetDenominator() > 0
context Tournament inv:
self.getMaxNumPlayers() > 0
context Temperature::Convert(ns) pre:
ns == 'c' || ns == 'C' || ns == 'f' || ns == 'F' || ns == 'k' || ns == 'K'
context LinkedList::Delete(p) post:
size() >= 0
Note that for invariants, you can use a special keyword
self, which represents any instance of a class.
Also note that the invariants illustrated in these last examples are
presented with method calls rather than direct member data. This is
because it's something the user can verify (remember, user doesn't have
access to private data
Throwing Exceptions