Object-Oriented Design & Patterns
Cay S. Horstmann
Chapter 3
The Object-Oriented Design Process
Chapter Topics
- An overview of the Date classes in the Java library
- Designing a Day class
- Three implementations of the Day class
- The importance of encapsulation
- Analyzing the quality of an interface
- Programming by contract
- Unit testing
The Importance of Encapsulation
- Even a simple class can benefit from different implementations
- Users are unaware of implementation
- Public instance variables would have blocked improvement
- Can't just use text editor to replace all
d.year
with
d.getYear()
How about
d.year++?
d = new Day(d.getDay(), d.getMonth(), d.getYear() + 1)
Ugh--that gets really inefficient in Julian representation
Don't use public fields, even for "simple" classes
Accessors and Mutators
- Mutator: Changes object state
- Accessor: Reads object state without changing it
- Day class has no mutators!
- Class without mutators is immutable
- String is immutable
- Date and GregorianCalendar are mutable
Don't Supply a Mutator for every Accessor
GregorianCalendar implements confusing rollover.
- Silently gets the wrong result instead of error.
Immutability is useful
Sharing Mutable References

Final Instance Fields
- Good idea to mark immutable instance fields as final
private final int day;
- final object reference can still refer to mutating object
private final ArrayList elements;
- elements can't refer to another array list
- The contents of the array list can change
Separating Accessors and Mutators
Yields current token and advances iteration
What if I want to read the current token again?
Even more convenient:
String getCurrent();
String next(); // returns current
Refine rule of thumb:
Mutators can return a convenience value, provided there is also an
accessor to get the same value
Side Effects
- Side effect of a method: any observable state change
- Mutator: changes implicit parameter
- Other side effects: change to
- explicit parameter
- static object
- Avoid these side effects--they confuse users
- Good example, no side effect beyond implicit parameter
a.addAll(b)
mutates a but not b
Advanced:
FieldPosition position = . . .;
Date d = formatter.parse(dateString, position);
Side effect: updates position parameter
Design could be better: add position to formatter state
Your classes may need to run in an environment without System.out
Rule of thumb: Minimize side effects beyond implicit parameter
Law of Demeter
- Example: Mail system in chapter 2
Mailbox currentMailbox = mailSystem.findMailbox(...);
- Breaks encapsulation
- Suppose future version of MailSystem uses a database
- Then it no longer has mailbox objects
- Common in larger systems
- Karl Lieberherr: Law of Demeter
- Demeter = Greek goddess of agriculture, sister of Zeus
- The law: A method should only use objects that are
- instance fields of its class
- parameters
- objects that it constructs with new
- Shouldn't use an object that is returned from a method call
- Remedy in mail system: Delegate mailbox methods to mail system
mailSystem.getCurrentMessage(int mailboxNumber);
mailSystem.addMessage(int mailboxNumber, Message msg);
. . .
- Rule of thumb, not a mathematical law
Quality of Class Interface
- Customers: Programmers using the class
- Criteria:
- Cohesion - class operations should support a single, coherent purpose
- Completeness - class should support all operations necessary to class' purpose
- Convenience - make common tasks easy
- Clarity - interface should be clear and easy to use
- Consistency - operations and their parameters should be consistently named, and their behaviors should be easily comprehensible from names
- Engineering activity: make tradeoffs
Cohesion
Completeness
How many milliseconds have elapsed?
No such operation in Date class
Does it fall outside the responsibility?
After all, we have before, after, getTime
Convenience
Why doesn't System.in have a readLine method?
After all, System.out has println.
Scanner class fixes inconvenience
Clarity
Iterate through list:
ListIterator<String> iterator = countries.listIterator();
while (iterator.hasNext())
System.out.println(iterator.next());
To remove first two elements, you can't just "backspace"
remove does not remove element to the left of iterator
From API documentation:
Removes from the list the last element that was returned by next or previous.
This call can only be made once per call to next or previous.
It can be made only if add has not been called after the last call to next or previous.
Huh?
Consistency
Why is month 0-based?
But
boolean regionMatches(int toffset,
String other, int ooffset, int len)
boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len)
Why not regionMatchesIgnoreCase?
Very common problem in student code
Programming by Contract
- Spell out responsibilities
- Increase reliability
- Increase efficiency
Preconditions
- Caller attempts to remove message from empty MessageQueue
- What should happen?
- MessageQueue can declare this as an error
- MessageQueue can tolerate call and return dummy value
- What is better?
- Excessive error checking is costly
- Returning dummy values can complicate testing
- Contract metaphor
- Service provider must specify preconditions
- If precondition is fulfilled, service provider must work correctly
- Otherwise, service provider can do anything
- When precondition fails, service provider may
- throw exception
- return false answer
- corrupt data
/**
Remove message at head
@return the message at the head
@precondition size() > 0
*/
Message remove()
{
return elements.remove(0);
}
- What happens if precondition not fulfilled?
- IndexOutOfBoundsException
- Other implementation may have different behavior
Circular Array Implementation
- Efficient implementation of bounded queue
- Avoids inefficient shifting of elements
- Circular: head, tail indexes wrap around
- Ch3/queue/MessageQueue.java
Inefficient Shifting of Elements

A Circular Array

Wrapping around the End

Preconditions
- In circular array implementation, failure of remove
precondition corrupts queue!
- Bounded queue needs precondition for add
- Naive approach:
@precondition size() < elements.length
- Precondition should be checkable by caller
- Better:
@precondition size() < getCapacity()
Assertions
Throws AssertionError if condition false and checking enabled
public Message remove()
{
assert count > 0 : "violated precondition size() > 0";
Message r = elements[head];
. . .
}
Or shorter, java -ea
Exceptions in the Contract
/**
. . .
@throws NoSuchElementException if queue is empty
*/
public Message remove()
{
if (count == 0)
throw new NoSuchElementException();
Message r = elements[head];
. . .
}
- Exception throw part of the contract
- Caller can rely on behavior
- Exception throw not result of precondition violation
- This method has no precondition
Postconditions
Postcondition of one call can imply precondition of another:
q.add(m1);
m2 = q.remove();
Class Invariants
- Condition that is
- true after every constructor
- preserved by every method
(if it's true before the call, it's again true afterwards)
- Useful for checking validity of operations
First check it's true for constructor
- Sets head = 0
- Need precondition size > 0!
Check mutators. Start with remove
What's the use? Array accesses are correct!
return elements[head];
Unit Testing
- Unit test = test of a single class
- Design test cases during implementation
- Run tests after every implementation change
- When you find a bug, add a test case that catches it
JUnit
- One popular tool for unit testing is JUnit.
- JUnit can be downloaded at http://junit.org/

JUnit
- Convention: Test class name = tested class name + Test
- Test methods start with test
import junit.framework.*;
public class DayTest extends TestCase
{
public void testAdd() { ... }
public void testDaysBetween() { ... }
. . .
}
- Each test case ends with assertTrue method
(or another JUnit assertion method such as assertEquals)
- Test framework catches assertion failures
public void testAdd()
{
Day d1 = new Day(1970, 1, 1);
int n = 1000;
Day d2 = d1.addDays(n);
assertTrue(d2.daysFrom(d1) == n);
}