Designing object initialization

Ensure proper initialization of your objects at all times

With this installment of Design Techniques, I begin a series of articles that will focus on the design of classes and objects. In this series I will discuss the following: designing classes for proper object initialization, finalization, and cleanup; designing fields and methods, in general; and designing class fields and methods, in particular. I won't address the design of class hierarchies in these first few installments -- rather, I will focus only on the design of individual classes.

Java and basic software design principles

In the first few installments of this column series, I plan to cover some basic object-oriented and structured design techniques as they apply to Java. Many of you undoubtedly are already familiar with these techniques, as they apply equally well to other languages. In my experience in the cubicle, however, I have encountered a lot of code written by programmers who, shall we say, could stand to take a refresher course on the basics. So I think it is important to cover the basics in the early articles of this column.

To maximize the usefulness of these first articles, I will be focusing on how the basic software design principles apply to Java in light of Java's architecture. For example, when I write about designing objects for proper cleanup, I'll discuss Java's finalization and garbage collection mechanisms and show how they affect design. When I write about designing with class variables and class methods, I'll describe Java's mechanisms for class loading and unloading and show how they affect design. For this article, which looks at designing objects for proper initialization, I've written an entire companion article that describes how object initialization works in the Java virtual machine (JVM). In all the articles of this column, I hope to show how the respective architectures of the Java language, virtual machine, and API affect how you should think about designing Java programs.

Viewing objects as finite state machines

One way to think of objects is as finite state machines. Thinking of objects in this way as you design classes can help you acquire a mindset that is conducive to good object design.

For example, one finite state machine is a simple traffic light that has three states: red, yellow, and green. A state transition diagram for such a traffic light is shown in Figure 1 (a Java applet).

You need a Java-enabled browser to see this applet.
State-transition diagram for a traffic light

In Figure 1, states are represented by labeled circles, state changes by arrows, and events by labels next to the arrows. When the finite state machine experiences an event, it responds by performing the state change indicated by the arrow.

The finite state machine shown in Figure 1 could be represented by instances of the following Java class. (As a matter of fact, the following class is used to represent the traffic light finite state machine in the Java applet that is Figure 1. Click here to view the full source code of the traffic light applet.)

public class TrafficLight {
    public static final int RED = 0;
    public static final int YELLOW = 1;
    public static final int GREEN = 2;
    private int currentColor = RED;
    public int change() {
        switch (currentColor) {
        case RED:
            currentColor = GREEN;
            break;
        case YELLOW:
            currentColor = RED;
            break;
        case GREEN:
            currentColor = YELLOW;
            break;
        }
        return currentColor;
    }
    public int getCurrentColor() {
        return currentColor;
    }
}

In class TrafficLight, the state of the object is stored in the currentColor private instance variable. A "change" event is sent to the object by invoking its public instance method, change(). State changes are accomplished through the execution of the code that implements the change() method.

As you can see from the TrafficLight example, Java objects map to finite state machines in the following ways:

  • The range of possible values that can be stored in the object's instance variables map to the finite state machine's states
  • An invocation of an object's instance method maps to a finite state machine receiving an event
  • Executing an object's instance method maps to the state change that a finite state machine experiences as the result of an event

Most objects you design will have many more states than a TrafficLight object. Some objects will have instance variables whose ranges are limited only by available resources, such as memory, rendering objects that are practically "infinite state machines." But in general, it is helpful to think of objects as state machines and to design them accordingly.

The canonical object design

The TrafficLight class is an example of the generally accepted form of a basic object design. The object has state, represented by instance variables that are private. The only way that code defined in other classes can affect the object's state is by invoking the object's instance methods.

Because other classes don't have direct access to the currentColor variable, they can't screw up its value, such as by setting it equal to 5. (In this case, the only valid values for currentColor are 0, 1, and 2.) In addition, there is no way for a TrafficLight object to go directly from state YELLOW to state GREEN, GREEN to RED, or RED to YELLOW.

Given this design of class TrafficLight, a TrafficLight object will always have a valid state and will always experience valid state transitions -- from the beginning of its lifetime to the end.

Designing objects for flexibility and robustness

Why should you care about designing good objects? One reason is that a set of robust objects can help contribute to the overall robustness of the program they constitute. In addition, well designed objects are more flexible (easier to understand and easier to change) than poorly designed objects.

A fundamental way to make your object designs robust and flexible is by ensuring that your objects have a valid state, and experience only valid state transitions, from the beginning of their lifetimes to the end. The rest of this article will discuss ways to ensure that classes begin their lifetimes with a valid state.

Introducing class CoffeeCup and the virtual café

The discussion of object design in this and subsequent articles will make use of a class that models coffee cups. You can imagine using this class in a program that implements a "virtual café": a place in cyberspace where guests can sit at small tables, sipping virtual cups of coffee and chatting with one another. The primary function of the café is that of a chat room, where people separated by (potentially vast) physical distances yet connected to the same network come together to converse. To make your chat room more compelling, you want it to look like a caf&eacute. You want each participant to see graphical representations ("avatars") of the other people in the caf&eacute. And, to make the participants' experience more real, you want the people to be able to interact with certain items in the café, such as tables, chairs, and cups of coffee.

The CoffeeCup

The basic CoffeeCup class has one instance variable, innerCoffee, which keeps track of the number of milliliters of coffee contained in the cup. This variable maintains your virtual coffee cup's state. The following methods allow you to change its state by:

  • Adding coffee to the cup (the add() method)
  • Removing one sip of coffee from the cup (the releaseOneSip() method)
  • Spilling the entire contents of the cup (the spillEntireContents() method)

Here is a class that represents a simple coffee cup in a virtual caf&eacute:

// In Source Packet in ex1/CoffeeCup.java
class CoffeeCup {
    private int innerCoffee;
    public void add(int amount) {
        innerCoffee += amount;
    }
    public int releaseOneSip(int sipSize) {
        int sip = sipSize;
        if (innerCoffee < sipSize) {
            sip = innerCoffee;
        }
        innerCoffee -= sip;
        return sip;
    }
    public int spillEntireContents() {
        int all = innerCoffee;
        innerCoffee = 0;
        return all;
    }
}

Some definitions

Before getting started with a discussion of design guidelines for object initialization, I'd like to clarify a few terms.

Designer vs. client programmers

If you're like most Java programmers, you alternate between two hats, which you wear at different times. Sometimes you wear your "designer" hat and build libraries of classes for others to use; other times you wear your "client" hat and make use of a library of classes created by someone else. Some Java programmers -- completely oblivious to the rules of fashion -- are known to wear both hats at the same time.

One aspect of the flexibility of a body of code is the ease with which a client programmer can understand the code. Whether a client programmer is planning to change code or just use it as is, that programmer often has to figure out how to change or use the code by reading it.

The guidelines discussed in the remainder of this article, and in subsequent articles of this column, will talk about flexibility in terms of client programmers. Designs and implementations that are flexible are those that are easy for client programmers to understand, use, and change.

Java jargon related to initialization

This article uses several terms related to initialization that are defined in precise ways by the Java Language Specification (JLS).

  • Default values are the values given to instance (and class) variables when they are first allocated on the heap -- before any initialization code is executed
  • A no-arg constructor is a constructor that takes no arguments
  • A default constructor is a no-arg constructor generated implicitly by the compiler for classes that don't have any constructors explicitly declared in the source file
  • An instance variable initializer is an equals sign (=) and expression sitting between an instance variable declaration and its terminating semicolon
  • An instance initializer is a block of code executed during object initialization in textual order along with instance variable initializers

For more information on these terms, see the companion article to this month's column, "Object initialization in Java."

Strategies for object initialization

When you design a class, you should attempt to ensure that all fields declared in the class are initialized to "proper" values, no matter how that object is created. Although Java's mechanisms for object initialization can help you achieve this goal, in the end, it's up to you to be sure to use the mechanisms correctly. By themselves, these mechanisms do not guarantee that classes you design will always be properly initialized.

Fortunately, as the designer of a class, you get to decide what initial values are deemed proper. If you decide that the default values for each instance variable declared in a class are proper, you needn't have any initializers or constructors at all. The important point is that the object begin its life with an internal state that yields proper behavior from then on. As mentioned previously, this is not only a goal of initialization but also of each state transformation the object can go through during its lifetime. If all your instance variables are private, only the methods of your class can transform the state of the object. By proper design of initializers, constructors, and methods of your class, you can ensure that an object of that class will always have a proper internal state -- from the beginning of its lifetime to the end.

Four approaches to initializing an instance variable

In the case of an instance variable for which the default value is not a proper initial state, you can take one of four approaches to initialization:

  1. Always assign it the same proper initial state via an initializer or constructor
  2. Calculate a proper initial state from data passed to a constructor
  3. Do either of the above, depending on which constructor is used to create the object
  4. Don't assign it a proper initial state and declare the object "invalid"

The first approach

By example:

// In Source Packet in ex2/CoffeeCup.java
// Approach 1
class CoffeeCup {
    private int innerCoffee = 355;
    // no constructors defined in this version of CoffeeCup
    //...
}

In this example, innerCoffee is always initialized to 355, so CoffeeCup objects will always begin life with 355 milliliters of coffee in them.

The second approach

Alternatively, you could require that client programmers using the CoffeeCup class pass in an initial starting value for innerCoffee:

// In Source Packet in ex3/CoffeeCup.java
// Approach 2
class CoffeeCup {
    private int innerCoffee;
    // Only one constructor defined in this
    // version of CoffeeCup
    public CoffeeCup(int startingAmount) {
        if (startingAmount < 0) {
            String s = "Can't have negative coffee.";
            throw new IllegalArgumentException(s);
        }
        innerCoffee = startingAmount;
    }
    //...
}

In this example, class CoffeeCup declares only one constructor, which takes an int parameter. Because a constructor is explicitly declared in CoffeeCup, the compiler won't generate a default constructor. Therefore, CoffeeCup does not have a no-arg constructor. As a result, client programmers who want to create a CoffeeCup object are forced to use the constructor that requires an int. They must supply an initial value for innerCoffee.

Checking for invalid data passed to constructors

1 2 3 Page
Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more