Recommended: Sing it, brah! 5 fabulous songs for developers
JW's Top 5
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 5 of 5
// In Source Packet in ex7/Example2.java
class Example2 {
public static void main(String[] args) {
// Create a "tall" coffee cup filled with 50 ml coffee.
CoffeeCup cup1 = new CoffeeCup(CoffeeCup.TALL, 50);
// Create an empty "short" coffee cup.
CoffeeCup cup2 = new CoffeeCup(CoffeeCup.SHORT);
}
}
This version of class CoffeeCup requires that the client programmer specify a cup size in both constructors. It doesn't have a no-arg constructor.
The fourth approach
One other option you could offer a client programmer is to create a CoffeeCup object with no specific size. Here, you would allow a client programmer to create a CoffeeCup object with a default constructor, but on creation the object would not be "valid" because it wouldn't have a valid size.
Later, that programmer would have to invoke a method to give the CoffeeCup object a proper size. Until it was given a proper size, the CoffeeCup object would be "invalid" and could not be used. Here is how you would implement this scheme:
// In Source Packet in ex8/InvalidObjectException.java
class InvalidObjectException extends Exception {
}
// An illustration of approach 4
class CoffeeCup {
public static final int SHORT = 0;
public static final int TALL = 1;
public static final int GRANDE = 2;
public static final int MAX_SHORT_ML = 237;
public static final int MAX_TALL_ML = 355;
public static final int MAX_GRANDE_ML = 473;
private boolean sizeValid;
private int size;
private int innerCoffee;
// Only three constructors defined in this
// version of CoffeeCup
public CoffeeCup() {
}
public CoffeeCup(int size) {
this(size, 0);
}
public CoffeeCup(int size, int startingAmount) {
if (startingAmount < 0) {
String s = "Can't have negative coffee.";
throw new IllegalArgumentException(s);
}
if (startingAmount > getMaxAmount(size)) {
String s = "Too much coffee.";
throw new IllegalArgumentException(s);
}
setSize(size);
innerCoffee = startingAmount;
}
public final void setSize(int size) {
if ((size != SHORT) && (size != TALL)
&& (size != GRANDE)) {
String s = "Invalid cup size.";
throw new IllegalArgumentException(s);
}
sizeValid = true;
this.size = size;
}
private static int getMaxAmount(int size) {
int retVal = 0;
switch (size) {
case SHORT:
retVal = MAX_SHORT_ML;
break;
case TALL:
retVal = MAX_TALL_ML;
break;
case GRANDE:
retVal = MAX_GRANDE_ML;
break;
default:
String s = "Invalid cup size.";
throw new IllegalArgumentException();
}
return retVal;
}
public int add(int amount) throws InvalidObjectException {
if (!sizeValid) {
throw new InvalidObjectException();
}
innerCoffee += amount;
int max = getMaxAmount(size);
int spillAmount = 0;
if (innerCoffee > max) {
spillAmount = innerCoffee - max;
innerCoffee = max;
}
return spillAmount;
}
//...
}
In this example, sizeValid indicates whether or not instance variable size has been set to a proper value. If the object is constructed with either of the two constructors that include a size in their parameter list (and the passed size is valid), sizeValid is set to true. If the object is created with the no-arg constructor, the size is not specified and sizeValid is left at its default initial value, false. Later, the size variable can be set to a proper value via the setSize() method, which also sets sizeValid to true.
This is not a very appropriate design for class CoffeeCup. Although you will probably encounter some situations in which it makes sense to design a class such that its objects can
at times be invalid, generally you should avoid this approach. As a guideline, you should attempt to design classes such that
their instances are always valid, from the beginning to the end of their lifetimes. To follow this guideline, you either initialize
instance variables to default proper values or require that the client programmer provide data in constructor parameters from
which you can calculate proper initial values.
Objects that can have invalid states are harder to use and harder to understand than those that are always valid. Usually,
when an object is in an invalid state, many of its methods won't work. In the example above, the add() method doesn't work when the CoffeeCup doesn't have a valid size. Here, the add() method uses both the size and innerCoffee variables to determine how much more coffee can legally be added to the cup. If the amount of coffee passed as a parameter
to add() plus the amount of coffee already in the cup exceeds the maximum capacity of the cup, the add() method will "spill" the extra coffee back to the caller through its return value. When sizeValid is false, the add() method can't determine the maximum capacity of the cup, because it doesn't know the cup's size. Hence, the add() method won't work when the size variable is invalid.
If you do design a class that has invalid states, you should throw an exception when a method can't perform normally because
its object is invalid. In the example above, the add() method throws an InvalidObjectException exception if sizeValid is false. This exception clearly indicates to the caller that the add() method did not work and why.
One final object initialization guideline involves the actual activities of constructors and initializers.
As a C++ programmer, I encountered a class one day that had no methods and no fields -- only a constructor. In trying to determine what this constructor did, I found that it called a function that called a function that called a function, and so on. After looking through several files, I realized that instantiating an object of this class caused a file to be parsed. After that the class was useless.
Granted, this particular class was designed by someone new to object-oriented programming, but its design illustrates an important point relating to constructors and initializers in Java: constructors and initializers should be about initialization.
In a previous section of this article, I recommended that you strive to design objects that give client programmers no way to instantiate the object in an invalid state. Another way to look at this is that client programmers should not have to do anything besides invoke a constructor to get an object in a proper initial state. An object should be fully initialized by the constructors and initializers of its class.
Likewise, just as objects should not be less than initialized after a constructor invocation, they also shouldn't be more than initialized. As a general principle, constructors and initializers should do no more than bring the new object to a proper initial state. To get the object moving after construction has brought it to a proper initial state, you should require that client programmers invoke a method on the object.
Two important design guidelines this article attempts to promote are the "canonical object design" and "omni-valid state principle."
The canonical object design
An object should have state, represented by instance variables that are private. Invoking an object's instance methods should
be the only way code defined in other classes can affect the object's state.
The omni-valid state principle
Objects should have a valid state, and experience only valid state transitions, from the beginning of their lifetimes to
the end.
With respect to object initialization, this article suggests a number of handy guidelines:
One last guideline pertains to the nature of guidelines themselves. The guidelines proposed in this column are not proposed as laws you should blindly follow at all times but as rules of thumb you'll probably want to follow much of the time. They are intended to help you acquire a mindset conducive to good design. Thus, the final guideline is:
Software design is subjective. Your idea of a well-designed program may be your colleague's maintenance nightmare. In light of this fact, I hope to make this column as interactive as possible.
I encourage your comments, criticisms, suggestions, flames -- all kinds of feedback -- about the material presented in this column. If you disagree with something, or have something to add, please let me know.
In next month's Design Techniques, I'll continue this mini-series of articles focusing on designing classes and objects. Next month's installment, the second of this series, will offer field and method design guidelines. :END_BODY
ENDNOTE: The small print: "Designing Object Initialization" Article Copyright (c) 1998 Bill Venners. All rights reserved. Traffic Light Applet Copyright (c) 1998 Bill Venners. All rights reserved. :END_ENDNOTE: