artima.com [an error occurred while processing this directive]

Part I. Objects
People-Oriented API Design
by Bill Venners

Object-oriented APIs are toolboxes for programmers. An API contains types and their members: classes to instantiate, classes to subclass, interfaces to implement, constants to use, and static methods to invoke. When you design an API, you design types. But the main utility of those types is providing client programmers with useful objects they can employ in their programs. The primary tool an API toolbox offers is objects.

To be a good API designer, therefore, you need a sense of what makes a good object design. In this chapter, I discuss a handful of object designs that I feel are fundamental. If you like big words, you can consider this chapter as a taxonomy of objects, a way to classify objects into categories such as service, messenger, performer, value, immutable. My goal for this chapter's material is to give you a feel for different kinds of object designs and the situations in which they are appropriate.

Guideline 1. Design objects for people, not for computers

The basic conceptual unit of object-oriented design is, not surprisingly, the object. It is therefore vital that designers of object-oriented APIs grasp the significance of the object. To me, the most important thing to keep in mind when designing objects is that objects are for people, not for computers. When you design objects, you should think primarily about the people who will use them.

The point of objects is not to help computers run software, but to help people develop software. Computers are just as happy running assembly language programs as object-oriented programs. But people are more productive (and with luck, happier) writing object-oriented programs. The main aim of software technology advances, from machine language to assembly to procedural to object-oriented languages, has been to help programmers do their jobs. In particular, objects help programmers manage the challenges of software complexity and change.

Managing Complexity

Moore's law says that computer hardware capability doubles every 18 months. For programmers, this is good news and bad news. The good news is that programmers get to work with ever more blazing machines. The bad news is that as hardware becomes more powerful, software becomes larger and more complex. One way objects assist programmers is by helping them manage software's increasing complexity .

Large software systems are difficult to understand. If a system is composed of individual object pieces, however, each object can embody an amount of complexity that programmers can fully grasp. Programmers can then understand the system's behavior as a whole in terms of the behavior of its object pieces and the interactions between them. A well-designed object, therefore, is understandable.

During an object-oriented design, you divide the system functionality into areas of responsibility. You assign each area of responsibility to a class, and give each class a name. For each named class, you devise a bundle of services (each service offered by a method) through which that class's instances fulfill their responsibilities. By focusing each object on an area of responsibility that encompasses a reasonable level of complexity, you help programmers who use your objects deal with the overall complexity of their systems.

Understanding is further enhanced through the practice of naming classes after relevant concepts from the problem domain, such as Account, Matrix, or StampDispenser. To the extent that classes model familiar real-world concepts from the problem domain, programmers can find it easier to understand and use instances of those classes.

Programmers can also find it easier to comprehend object-oriented systems because their organization is similar to that of human activities. If you have a goal, you can hire people to help you achieve that goal. Each person agrees to perform a particular job. You organize and direct their individual efforts to help you achieve your overall goal. Similarly, to accomplish a goal in a system, you enlist the help of objects. Each object must fulfill the obligations delineated in its contract. By organizing and directing the services provided by the objects, you can achieve your overall goal for the system.

Objects also help programmers manage complexity by facilitating communication. Types have names that have meaning to the programmers working on a system. A type name implies an areas of responsibility. It implies the methods through which instances fulfill those responsibilities. It imples intended usage, performance characteristics, and so on. As programmers discover, name, and define types during an object-oriented design, a vocabulary develops. This vocabulary, comprised of type names and their meanings, facilitates communication among the programmers. The more effectively programmers can communicate about their system, the more effectively they can tame its complexity.

Lastly, objects help programmers create systems that, despite being complex, are robust. When you organize your system with objects, you tend to encapsulate code that manipulates particular data inside the class that contains the data. This keeps code that operates on particular data from replicating and spreading throughout the system. If you detect a bug that corrupts certain data, you need only look in one place to find the bug: the code that defines the class's methods. Once you fix that bug in the class's code, you know it is fixed everywhere, because that code is the only code that can manipulate the data. In addition, a class is a good unit for unit testing. By regularly running a suite of unit tests on the classes that make up your system, you increase the likelihood of finding and fixing bugs early. The tools of encapsulation and unit testing help you build objects that are thoroughly debugged and, therefore, reliable and robust. To build a robust system, you must to the extent possible construct the system out of robust parts. Objects help programmers build systems that, despite their overall complexity, are robust because their systems are constructed out of robust object parts.

Managing Change

Besides complexity, another fundamental challenge of software development is change. If a software project doesn't fail initially, the resulting code base tends to have a long life. With each new release comes new requirements. Existing code is tweaked and enhanced to fix bugs and add functionality. Objects, in addition to helping programmers manage complexity, help programmers manage change.

One ideal of object-oriented programming is a strong separation of interface and implementation. The primary enemy of change in a software system is coupling, the interdependencies between various system parts. The aim of separating interface and implementation is to help programmers minimize coupling in their systems. At a keynote address I saw Bill Joy give at the Software Developer conference in Washington, D.C., Joy described coupling by saying, "You slap your hand on this table in Washington and a building falls down in San Francisco." In other words, you make a small, seemingly innocuous change in one part of your system, and you inadvertently cause a disaster in a remote and unrelated part of your system. In an object-oriented system, object interfaces are the point of coupling between different system parts. Because interfaces are the only point of coupling between the parts, you can make many kinds of changes to implementations without breaking the expectations of client code. When you slap the implementation of a Table object in Washington, all the Building objects in San Francisco continue to stand tall.

Objects also help programmers deal with change by being replaceable modules. Polymorphism and dynamic binding enable you to unplug one implementation of an object interface and plug in a different implementation of that interface. This makes it easy to change a system by defining a new class that extends an existing class or implements an existing interface. You can instantiate the new class and pass the resulting object to existing code that knows only of the supertype.

Lastly, objects help programmers manage change because object contracts can be very abstract. An object's contract, the human language description of what the object promises to do when you invoke its instance methods, is usually expressed in terms of behavior. Instance data is kept private. The structure of instance data and code of instance methods do not appear as part of the object's contract. Contracts expressed in terms of behavior can be very abstract, simply because you can be vague when you describe behavior without specifying particular data structures or code algorithms. The higher the level of abstraction in a contract, the more options programmers have when changing an implementation, or plugging in a new implementation, of a class.

Monkeys on Your Back

In the real world, as you work to design and implement software, you have several concerns to keep in mind -- several "monkeys on your back." Each monkey competes with the others for your attention, trying to convince you to take its particular concern to heart as you work. One large, heavy monkey hangs on your back with its arms around your neck and repeatedly yells, "You must meet the schedule!" Another monkey, this one perched on top of your head (as there is no more room on your back), beats its chest and cries, "You must accurately implement the specification!" Still another monkey jumps up and down on top of your monitor yelling, "Robustness, robustness, robustness!" Another keeps trying to scramble up your leg crying, "Don't forget about performance!" And every now and then, a small monkey peeks timidly at you from beneath the keyboard. When this happens, the other monkeys become silent. The little monkey slowly emerges from under the keyboard, stands up, looks you in the eye, and says, "You must make the code easy to read and easy to change." With this, all the other monkeys scream and jump onto the little monkey, forcing it back under the keyboard. As you sit there in your cubicle and work on your software, to which monkey should you listen? Alas, in most cases you must listen to all of them. To do a "good" job, you will need to find a way to keep all these monkeys happy -- to strike a proper balance between these often conflicting concerns.

An object design is good to the extent it achieves the optimum balance among the concerns pressing on the design. And that proper balance depends on the situation. There are times when you should design, times when you should hack, and times when you should do something in-between.

In my experience, however, the most important monkey in object design is usually the monkey under the keyboard. This monkey gently reminds you to make the software easy to understand and change -- easy for people to understand, easy for people to change. When you hack, you are concerned about telling a computer what to do. When you design, you should be primarily concerned about communicating with other programmers.

Objects are for people. The reason objects exist is to help human programmers do their jobs. This is important to keep in mind when designing APIs as well as objects, because if objects are for people, then so are object interfaces. (After all, API means application programmer interface.) When you design an object or API, you are primarily designing for the benefit of human programmers.

Guideline 3. Understand the kinship between objects and state machines

In Guideline 2, I suggest you think of objects as machines. To get a better feel for the object as machine metaphor, I think it's helpful to look at the relationship between objects and state machines. Among the most useful insights that originally helped me get a feel for object design was that many objects act like state machines. You can think of all objects as machines. The kind of machine mutable service-oriented objects most resemble, mathematically speaking at least, is the state machine.

The Nature of the Machine

A state machine is defined by:

When a state machine is in some way built and put into motion, it starts out its lifetime in its initial state. At any time during its life it has a current state. The outside world interacts with the state machine by sending it messages. When the state machine receives a message, it performs actions, including potentially changing state.

Similarly, when an object is instantiated, it begins its lifetime in some initial state, established by the object's constructor. At any time during its life, it has a current state. The outside world interacts with the object by invoking its methods. When a method is invoked, the object performs actions, including potentially changing state, and then returns.

A Stamp Dispenser State Machine

Occasionally, you can describe the behavior of objects in state machine terms. For example, imagine an object that models the behavior of an extremely simple stamp dispenser described by these requirements:

Write control software for an automated stamp dispenser. The stamp dispenser accepts only nickels (5 cents) and dimes (10 cents) and dispenses only 20-cent stamps. The stamp dispenser's LED display indicates to the user the total amount of money inserted so far. As soon as 20 or more cents is inserted, a 20-cent stamp automatically dispenses along with any change. The only amounts the display shows, therefore, are 0, 5, 10, and 15 cents. If a dime and a nickel have been inserted, the display indicates 15 cents. If the user then inserts another dime, the stamp dispenser dispenses a 20-cent stamp, returns a nickel, and changes the display to show 0 cents. In addition to a coin slot, an opening for dispensing stamps and change, and an LED display, the stamp dispenser also has a coin return lever. When the user presses coin return, the stamp dispenser returns the amount of money indicated on the display, and changes the display to show 0 cents.

You could also describe the behavior of this simple stamp dispenser in terms of a state machine that has:

A stamp dispenser's current state indicates how much money has been inserted. If no money has been inserted, the stamp dispenser is in HAS_0 state. If a nickel has been inserted, the stamp dispenser is in HAS_5 state, and so on. No HAS_20 state appears in the list, because as soon as 20 cents is inserted, a stamp is automatically issued and any change is returned.

The three messages represent the actions a stamp dispenser user can take: inserting a nickel (add5), inserting a dime (add10), or pressing the coin return lever (returnCoins). The four actions the stamp dispenser can take are return a nickel (ret5), return a dime (ret10), return 15 cents (ret15), or dispense a 20 cent stamp (dispenseStamp).

Figure 3-1 shows the state-transition diagram that defines the actions taken and next state that results from a stamp dispenser receiving each message in each state.

Figure 3-1. The stamp dispenser state-transition diagram

In Figure 3-1, each of the four states, HAS_0, HAS_5, HAS_10, and HAS_15, is represented by a circle. The circle labeled start with an arrow pointing to the HAS_0 state indicates the state machine's initial state is HAS_0. State transitions are shown by arrows between states. Each arrow is labeled with the message that causes the transition and, if any actions are required to accompany the state transistion, a forward slash plus the required actions. For example, an arrow from HAS_10 to HAS_0 is labeled add10/dispenseStamp. This arrow indicates that if an add10 message is received while the state machine is in the HAS_10 state, the machine should change to the HAS_0 state and perform the dispenseStamp action.

A State Machine Object

The behavior of the simple stamp dispenser, described previously in both human-language and state machine terms, is exhibited by instances of the StampDispenser class shown in Listing 3-1.

Listing 3-1. Class StampDispenser

package com.artima.examples.stampdispenser.ex4;

import java.util.Set;
import java.util.Iterator;
import java.util.HashSet;

/**
* A stamp dispenser that accepts nickels and dimes and dispenses twenty cent
* stamps.
*
* @author Bill Venners
*/

public class StampDispenser {

    private final static int STAMP_VALUE = 20;
    private int balance;

    /**
    * Constructs a new stamp dispenser with a starting balance of zero.
    */

    public StampDispenser() {
    }

    /**
    * Add either 5 or 10 cents to the stamp dispenser. If the amount added
    * causes the balance to become or exceed 20 cents, the price of a stamp,
    * the stamp will be automatically dispensed. If the stamp is dispensed,
    * the amount of the balance after the stamp is dispensed is returned to
    * the client.
    *
    * @throws IllegalArgumentException if passed <code>amount</code> doesn't
    *    equal either 5 or 10
    */

    public synchronized void add(int amount) {

        if ((amount != 5) && (amount != 10)) {
            throw new IllegalArgumentException();
        }

        balance += amount;

        if (balance >= STAMP_VALUE) {

            // Dispense a stamp and return any change
            // balance - STAMP_VALUE is amount in excess of twenty cents
            // (the stamp price) to return as change. After dispensing the
            // stamp and returning any change, the new balance will be zero.
            System.out.print("Dispense stamp");
            int toReturn = balance - STAMP_VALUE;
            if (toReturn > 0) {
                System.out.println(", return " + toReturn + " cents.");
            }
            else {
                System.out.println(".");
            }
            balance = 0;
        }
    }

    /**
    * Returns coins. If the balance is zero, no action is performed.
    */

    public synchronized void returnCoins() {

        // Make sure balance is greater than zero, because no event should
        // be fired if the coin return lever is pressed when the stamp
        // dispenser has a zero balance
        if (balance > 0) {

            // Return the entire balance to the client
            System.out.println("Return " + balance + " cents.");
            balance = 0;
        }
    }
}

Class StampDispenser has one private instance variable, balance, which maintains the state of the object. Like the stamp dispenser state machine, StampDispenser objects can be in one of four possible states. The four states of the state machine correspond to value of balance in this way:

The StampDispenser class has one public no-arg constructor that ensures all instances begin life with an initial balance of zero. (Because the constructor contains no code, it leaves balance at its default initial value of zero.) A zero balance corresponds to the HAS_0 state, the initial starting state of the stamp dispenser state machine.

The interface of class StampDispenser includes two methods, add and returnCoins. Given a variable stampDispenser that is a reference to a StampDispenser object, invoking the add and returnCoins methods corresponds to sending messages to the state machine in this way:

The Role of State Machines

On occasion, software requirements are specified with formal state machines. For example, the Java Telephony API specification includes a state-transition diagram that defines the states through which a single call can progress. Nevertheless, software requirements are most often specified with human language descriptions, not with state machines.

The complexity of most objects makes it impractical to define their behavior solely in terms of state machines. The stamp dispenser's behavior can be specified in terms of a state machine primarily because it is a contrived example. I purposely chose this example to illustrate the kinship between mutable objects and state machines. The requirements of a real-world stamp dispenser would be far more complex, and therefore far less practical to describe in terms of a state machine. Most objects simply have too many possible states.

The StampDispenser has one instance variable, balance, which is an int. The class enforces that this int will ever have only one of four possible values: 0, 5, 10, or 15. It is practical to fully describe the behavior of StampDispenser objects in terms of a state machine because the object has only four possible states. If the class where written such that the balance variable could take on any possible value for an int, the class would have 2**32 possible states. That's already too many states to make it practical to describe the class's behavior fully in terms of a state machine. Even more impractical, describing the behavior of many mutable objects in exclusively state machine terms would require in effect an infinite number of possible states.

I bring up state machines to flesh out the object-as-machine metaphor -- to give you a better feel for the kind of machine you are designing when you design an object. Mutable service-oriented objects act like state machines, often with an infinite number of possible states. Invoking a method on an object corresponds to sending a message to a state machine. When a method is invoked, the object, like the state machine, potentially performs actions and changes state.

I will discuss the relationship between objects and state machines further in the context of the state pattern in Guideline ?.

Guideline 4. See objects as bundles of behavior, not bundles of data

One of the most basic object-oriented ideas is encapsulation -- associating data with code that manipulates the data. The data, stored in instance variables, represents the object's state. The code, stored in instance methods, represents the object's behavior. Because of encapsulation, therefore, you can think of objects as either bundles of data, bundles of behavior, or both. To reap the greatest benefit from encapsulation, however, you should think of objects primarily as bundles of behavior, not bundles of data. You should think of objects less as carriers of information, embodied in the data, and more as providers of services, represented by the behavior.

Why should you think of objects as bundles of services? If data is exposed, code that manipulates the data spreads across the program. If higher-level services are exposed, code that manipulates the data concentrates in one place: the class. This concentration reduces code duplication, localizes bug fixes, and helps you achieve robustness.

A Data-Oriented Matrix

Consider the Matrix class shown in Diagram 4-1. Instances of this Matrix class act more like bundles of data than bundles of behavior. Although the instance variables declared in this class are private, the only services it offers besides equals, hashcode, clone, and toString are accessor methods set, get, getCols, and getRows. These accessor methods are data oriented. They do nothing interesting with the object's state; they merely provide clients access to that state.

Diagram 4-1. A data-oriented Matrix

com.artima.examples.matrix.ex1
Matrix
public class Matrix implements java.io.Serializable, Cloneable
    Represents a matrix each of whose elements is an int.
Constructors
public Matrix(int rows)
    Construct a new square zero matrix whose order is determined by the passed number of rows.
public Matrix(int rows, int cols)
    Construct a new zero matrix whose order is determined by the passed number of rows and columns.
public Matrix(int[][] init)
    Construct a new Matrix whose elements will be initialized with values from the passed two-dimensional array of ints.
Methods
public Object clone()
    Clones this object.
public boolean equals(Object o)
    Compares passed object to this Matrix for equality.
public int get(int row, int col)
    Returns the element value at the specified row and column.
public int getCols()
    Returns the number of columns in this matrix.
public int getRows()
    Returns the number of rows in this matrix.
public int hashcode()
    Computes the hash code for this Matrix.
public void set(int row, int col, int value)
    Sets the element value at the specified row and column to the passed value.
public String toString()
    Returns a String that contains the integer values of the elements of this Matrix.

Listing 4-1 shows Example1, a client of the data-oriented Matrix. This client adds two matrices and prints the sum to the standard output.

Listing 4-1. A client of the data-oriented Matrix

package com.artima.examples.matrix.ex1;

class Example1 {

    public static void main(String[] args) {

        int[][] init1 = { {2, 2}, {2, 2} };
        int[][] init2 = { {1, 2}, {3, 4} };

        Matrix m1 = new Matrix(init1);
        Matrix m2 = new Matrix(init2);

        // Add m1 & m2, store result in a new Matrix object
        Matrix sum = new Matrix(2, 2);
        for (int i = 0; i < 2; ++i) {
            for (int j = 0; j < 2; ++j) {
                int addend1 = m1.get(i, j);
                int addend2 = m2.get(i, j);
                sum.set(i, j, addend1 +  addend2);
            }
        }


        // Print out the sum
        System.out.println("Sum: " + sum.toString());
    }
}

To add the matrices, Example1 first instantiates a matrix to hold the sum. Then for each row and column, Example1 invokes get on each addend matrix, adds the two returned values, and enters the result into the sum matrix using set.

This all works fine, but imagine you need to add matrices at 50 different places in your code. Example1 requires eight lines of code to add two matrices, shown highlighted in Listing 4-1. You would need to repeat those eight lines of code, or something similar, at 50 different places in your code. Perhaps the code performs flawless matrix addition in 46 of those places, but the other four places contain bugs. If you detect and fix a bug in one of those four places, you still have three matrix addition bugs lurking elsewhere.

A Service-Oriented Matrix

Now consider Diagram 4-2, which you can consider a second iteration in the design of class Matrix. In this iteration of Matrix, the previous iteration's set method has been replaced by more service-oriented methods: add, subtract, and multiply.

Diagram 4-2. A service-oriented Matrix

com.artima.examples.matrix.ex2
Matrix
public class Matrix implements java.io.Serializable, Cloneable
    A two-dimensional matrix of ints.
Constructors
public Matrix(int rows)
    Construct a new square Matrix whose order is determined by the passed number of rows.
public Matrix(int rows, int cols)
    Construct a new zero matrix whose order is determined by the passed number of rows and columns.
public Matrix(int[][] init)
    Construct a new Matrix whose elements will be initialized with values from the passed two-dimensional array of ints.
Methods
public Matrix add(Matrix addend)
    Adds the passed Matrix to this one.
public Object clone()
    Clones this object.
public boolean equals(Object o)
    Compares passed Matrix to this Matrix for equality.
public int get(int row, int col)
    Returns the element value at the specified row and column.
public int getCols()
    Returns the number of columns in this Matrix.
public int getRows()
    Returns the number of rows in this Matrix.
public int hashCode()
    Computes the hash code for this Matrix.
public Matrix multiply(int scalar)
    Multiplies this matrix by the passed scalar.
public Matrix multiply(Matrix multiplier)
    Multiplies this Matrix (the multiplicand) by the passed Matrix (the multiplier).
public Matrix subtract(Matrix subtrahend)
    Subtracts the passed Matrix from this one.
public String toString()
    Returns a String that contains the integer values of the elements of this Matrix.

The data required for matrix addition sits inside instances of class Matrix in the elements instance variable. In this second iteration, the code that performs matrix addition has moved to the class that contains the data. In the previous iteration, this code existed outside class Matrix, as demonstrated by the Example1 client of Listing 4-1. This code now shows up in the Matrix class's add method, shown in Listing 4-2.

Listing 4-2. The add method of the service-oriented Matrix

package com.artima.examples.matrix.ex2;

//...

public class Matrix implements Serializable, Cloneable {

    private int[][] elements;

    //...

    public Matrix add(Matrix addend) {

        int rowCount = getRows();
        int colCount = getCols();

        // Make sure addend has the same order as this matrix
        if ((addend.getRows() != rowCount)
            || (addend.getCols() != colCount)) {

            throw new IllegalArgumentException();
        }

        Matrix retVal = new Matrix(elements);
        for (int row = 0; row < rowCount; ++row) {
            for (int col = 0; col < colCount; ++col) {
                retVal.elements[row][col] += addend.elements[row][col];
            }
        }
        return retVal;
    }

    //...

}

Moving the addition code to the Matrix class means clients need not perform the add service themselves. Instead, clients can ask the Matrix object to perform that service for them. Clients can now delegate responsibility for matrix addition to Matrix, the class that has the data required for addition.

For example, consider the Example2 client shown in Listing 4-3. Example2 performs the same function as Example1: it adds two matrices and prints the result. But Example2 is a client of the service-oriented Matrix.

Listing 4-3. A client of the service-oriented Matrix

package com.artima.examples.matrix.ex2;

class Example2 {

    public static void main(String[] args) {

        int[][] init1 = { {2, 2}, {2, 2} };
        int[][] init2 = { {1, 2}, {3, 4} };

        Matrix m1 = new Matrix(init1);
        Matrix m2 = new Matrix(init2);

        // Add m1 & m2, store result in a new matrix object
        Matrix sum = m1.add(m2);

        // Print out the sum
        System.out.println("Sum: " + sum.toString());
    }
}

Now if you must add matrices at 50 places in your code, you need only repeat Example2's one liner, shown highlighted in Listing 4-3. If you discover a bug in matrix addition that corrupts matrix data, you know where to look: the add method of class Matrix. Once you fix that bug, it is in effect fixed at all 50 places where your code performs matrix addition. This is how seeing objects as bundles of services, not bundles of data, helps you achieve robustness.

Objects as Bundles of Services

Now, you may say that this is obvious. That I was simply factoring out duplicate code into a single method that everyone calls. That's true, but when you perform an object-oriented design, you in effect perform this code-to-data refactoring ahead of time.

During a object-oriented design's initial stages, you discover objects. You assign each object an area of responsibility and flesh out the services each type of object should provide. Finally, you design the interfaces through which objects provide their services to clients. In the process, you move code to data.

For example, you might decide to include in your solution a Matrix object responsible for matrix mathematics. As you flesh out the details, you decide that the Matrix class should handle matrix addition, matrix subtraction, and scalar and matrix multiplication. You then design an interface through which the Matrix can fulfill its responsibilities, such as the interface of the service-oriented Matrix class. By discovering the service-oriented Matrix in the initial design phase, rather than starting with the data-oriented Matrix and later refactoring towards the service-oriented Matrix, you in effect move code to data during the design process.

Moving code to data can yield objects that seem counterintuitive to beginners. In Guideline 1, I mentioned that object-oriented programming enables you to think more in terms of the problem domain. However, what you might do to an object in the real world, you often ask an object to do to itself in an object-oriented program. For example, in the real world you might multiply a matrix by -1, but in an object-oriented program, you might instead ask a Matrix object to multiply itself by -1. The reason you ask a Matrix to multiply itself is because matrix multiplication involves matrix data. Therefore, the code that represents the matrix multiplication know-how belongs in the class that holds the matrix data, the Matrix class itself. Although it may seem counterintuitive to ask a Matrix to multiple itself by -1, a Test to grade itself, or a String to trim white space off of itself, such requests are normal in object-oriented systems.

A Service-Oriented Mindset

Data-oriented methods, such as the accessor methods that appear in the data-oriented Matrix, are not inherently bad. In many situations they are appropriate. One common use of accessor methods is to access data inside objects used to transmit information. I discuss such data-oriented objects, called messengers, in Guideline 7.

Accessor methods are also commonly used to manipulate an object's properties. I think of properties as a special kind of data used to configure otherwise service-oriented objects. Properties are used heavily in JavaBeans, Java's component model, but also appear in non-JavaBean objects. I discuss configurable objects in Guideline ?.

A third use of accessor methods is to give client programmers access to an object's internal state, so they can do something with that state they can't via the object's methods. For example, no method of the service-oriented Matrix calculates and returns the transpose, the new matrix that results from interchanging an existing matrix's rows and columns. Nevertheless, client programmers that need a transpose can get the elements of the service-oriented Matrix via its get method and calculate the transpose themselves. I discuss this use of accessor methods in Guideline ?: Make common things easy, rare things possible.

Although many reasonable uses of data-oriented methods exist, you should maintain a service-oriented mindset when designing object methods. In general, you should design methods that do something interesting with the object's data --something more than just providing clients access to the data. In the process, you'll move code that knows how to manipulate data to the object that contains the data. Moving code to data offers you one of the prime benefits of object-oriented programming: a shot at robustness.

Guideline 6. Design Experts that use their state to decide how to behave

The basic and most common object design, the expert, has state, stored in instance variables, and behavior, contained in instance methods. An expert can be mutable or immutable. You can ask an expert to provide a service for you by invoking one of its methods. The method provides the service by taking actions, possibly changing the object's state, and returning.

A Stamp Dispenser Example

For an example of a mutable expert, imagine an object that models the behavior of an extremely simple stamp machine described by these requirements:

Write control software for an automated stamp dispenser. The stamp dispenser accepts only nickels (5 cents) and dimes (10 cents) and dispenses only 20-cent stamps. The stamp dispenser's LED display indicates to the user the total amount of money inserted so far. As soon as 20 or more cents is inserted, a 20-cent stamp automatically dispenses along with any change. The only amounts the display shows, therefore, are 0, 5, 10, and 15 cents. If a dime and a nickel have been inserted, the display indicates 15 cents. If the user then inserts another dime, the stamp dispenser dispenses a 20-cent stamp, returns a nickel, and changes the display to show 0 cents. In addition to a coin slot, an opening for dispensing stamps and change, and an LED display, the stamp dispenser also has a coin return lever. When the user presses coin return, the stamp dispenser returns the amount of money indicated on the display, and changes the display to show 0 cents.

An object-oriented solution to these requirements could include a class StampDispenser, shown in Diagram 6-1, which models the functionality of the real-world stamp dispenser. The StampDispenser offers its primary services to clients via two public methods: add and returnCoins.

Diagram 6-1. A simple stamp dispenser

com.artima.examples.stampdispenser.ex1
StampDispenser
public class StampDispenser
    A stamp dispenser that accepts nickels and dimes and dispenses twenty cent stamps.
Constructors
public StampDispenser()
    Constructs a new stamp dispenser with a starting balance of zero.
Methods
public synchronized void add(int amount)
    Add either 5 or 10 cents to the stamp dispenser.
public synchronized void addStampDispenserListener(StampDispenserListener l)
    Adds the specified stamp dispenser listener to receive stamp dispenser events from this stamp dispenser.
public synchronized void removeStampDispenserListener(StampDispenserListener l)
    Removes the specified stamp dispenser listener so that it no longer receives stamp dispenser events from this stamp dispenser.
public synchronized void returnCoins()
    Returns coins.

The StampDispenser also enables clients to be notified of events by registering themselves via the addStampDispenserListener method. Clients can change their minds via the removeStampDispenserListener method. Both of these methods take a StampDispenserListener parameter, an interface shown in Diagram 6-2.

Diagram 6-2. The StampDispenserListener interface

com.artima.examples.stampdispenser.ex1
StampDispenserListener
public interface StampDispenserListener
    Listener interface for receiving stamp dispenser events.
Methods
public void coinAccepted(StampDispenserEvent e)
    Invoked when coins have been accepted but no stamp has been dispensed.
public void coinsReturned(StampDispenserEvent e)
    Invoked when coins have been returned as the result of the returnCoins method being invoked on a StampDispenser.
public void stampDispensed(StampDispenserEvent e)
    Invoked when a stamp has been dispensed.

Were you to use a StampDispenser instance to control an actual stamp dispenser, the listeners would be responsible for actually returning coins, dispensing stamps, and changing the display. The client that invokes the add method would be code that knows money was inserted. The client that invokes the returnCoins method would be code that knows the return coins lever was pressed.

The StampDispenserListener methods indicate the stamp dispenser has accepted a coin, returned a coin, or dispensed a stamp. Each of these methods takes a StampDispenserEvent, shown in Diagram 6-3, which lets the listener determine the amount of money that should be returned, if any, and the current balance the display should show.

Diagram 6-3. The StampDispenserEvent class

com.artima.examples.stampdispenser.ex1
StampDispenserEvent
public class StampDispenserEvent extends java.util.EventObject
    Event that indicates a stamp dispenser has performed an action.
Constructors
public StampDispenserEvent(StampDispenser source, int amountReturned, int balance)
    Constructs a StampDispenserEvent with amountReturned, and balance.
Methods
public int getAmountReturned()
    Returns the amount of money returned to the client, expressed in units of American pennies.
public int getBalance()
    Returns the current balance: the amount of money that has been inserted into the stamp dispenser, but not returned via a coin return or consumed in exchange for a dispensed stamp.

The Expert Object Design

Class StampDispenser illustrates the basic form of a service-oriented object. Its instance variables are private, so its accessible methods are the only way to manipulate the state of the object. Although service-oriented objects like StampDispenser have state, they use their state to decide how to behave when their methods are invoked. Consider the code of StampDispenser's add method, shown in Listing 6-1.

Listing 6-1. The add method of StampDispenser

package com.artima.examples.stampdispenser.ex1;

//...

public class StampDispenser {

    private final static int STAMP_VALUE = 20;
    private int balance;

    //...

    public synchronized void add(int amount) {

        if ((amount != 5) && (amount != 10)) {
            throw new IllegalArgumentException();
        }

        balance += amount;

        if (balance >= STAMP_VALUE) {


            // Dispense a stamp and return any change
            // balance - STAMP_VALUE is amount in excess of twenty cents
            // (the stamp price) to return as change. After dispensing the
            // stamp and returning any change, the new balance will be zero.
            StampDispenserEvent event = new StampDispenserEvent(this,
                balance - STAMP_VALUE, 0);
            balance = 0;
            fireStampDispensed(event, listenersClone);
        }
        else {


            // Fire an event to indicate the balance has increased
            StampDispenserEvent event = new StampDispenserEvent(this,
                amount, balance);
            fireCoinAccepted(event, listenersClone);
        }
    }

    //...
}

StampDispenser's balance variable keeps track of the amount of money inserted but not returned or exchanged for stamps. StampDispenser offers no data-oriented methods to set and get the balance. Rather, it uses the balance to decide how to behave when its service-oriented methods add and returnCoins are invoked. As shown by the highlighted portions of Listing 6-1, when the StampDispenser's add method is invoked, it uses its current balance to decide whether or not to dispense a stamp, whether or not to return any change, and what new value to give to balance.

This guideline's main point is that in the basic, expert object design, objects keep their state private and expose only their behavior. The state can be either mutable or immutable. The reason such objects have state is to help them decide how to behave when called upon to perform a service. Thus, even though such objects have both state and behavior, they are service-oriented, not data-oriented.

Guideline 7. Design Messengers when you don't know the behavior

In Guideline 4, I encourage you to think of objects of bundles of services, not bundles of data. In Guideline 6, however, I point out that in practice some objects are bundles of data anyway. Such objects, which I call messengers, show up at the state end of the state-behavior spectrum shown in Figure 5-1. In this guideline, I explain why it sometimes makes sense to disregard Guideline 4 and create messengers.

A messenger is an object that allows you to package and send data. Often data is passed to a messenger's constructor, and the messenger is sent along its way. Recipients of the messenger access the data via accessor methods, which in Java usually take the form get. Messengers are usually short-lived objects. Once a recipient retrieves the information contained in a messenger, it usually kills the messenger (even if the news is good).

In general, you should move code to data as described in Guideline 4. Most of your object designs should be service-oriented objects, as described in Guideline 6. But on occasion, you may find yourself with some data that you don't know what to do with. Sometimes you know some information, but you don't know what behavior that information implies. You can't move the code to the data, because even though you know the data, you don't know what the code should do. In such cases, you can encapsulate the data inside a messenger object, and send the messenger to some recipient that does know the behavior implied by the data. The recipient extracts the data from the messenger and takes appropriate action.

Exceptions

One common example of messengers is exceptions. Exception objects are generally composed of a small amount of data, which is passed to the constructor, and some accessor methods that let catch clauses access the data. Like most messengers, exceptions usually have short lives. They are created when an abnormal condition is encountered, thrown up the call stack, caught by an application or default catch clause, handled, and discarded. Diagram 7-1 shows an example of an exception class.

Diagram 7-1. The InsufficientFundsException is a messenger

com.artima.examples.account.ex3
InsufficientFundsException
public class InsufficientFundsException extends Exception
    Exception thrown by Accounts to indicate that a requested withdrawal has failed because of insufficient funds.
Constructors
public InsufficientFundsException(long shortfall)
    Constructs an InsufficientFundsException with the passed shortfall and no specified detail message.
public InsufficientFundsException(String message, long shortfall)
    Constructs an InsufficientFundsException with the passed detail message and shortfall.
Methods
public long getShortfall()
    Returns the shortfall that caused a withrawal request to fail.

An InsufficientFundsException contains an optional message and a required shortfall. The exception sender passes this data to a constructor. The exception recipient (a catch clause) can retrieve the data via accessor methods. Often, the most important piece of information carried in an exception is embodied in the name of the exception class. Exception class names usually indicate the kind of abnormal condition encountered. In this case, the name InsufficientFundsException indicates that someone attempted to withdraw more money than was available in their account. Any data stored in the exception object generally adds more detailed information about the abnormal condition described by the exception class name.

InsufficientFundsException may be thrown by the withdraw method of class OverdraftAccount, shown in Diagram 7-2. The reason a bundle of data makes sense in the InsufficientFundsException case is that the designer of the withdraw method doesn't know how to deal with the situation that triggers the exception. The designer knows someone has attempted to withdraw more money than is available in their account, but doesn't know what behavior is appropriate.

Diagram 7-2. The OverdraftAccount class

com.artima.examples.account.ex3
OverdraftAccount
public class OverdraftAccount
    Represents a bank account with overdraft protection.
Constructors
public OverdraftAccount(long overdraftMax)
    Constructs a new Account with the passed overdraft maximum.
Methods
public void addOverdraftListener(OverdraftListener l)
    Adds the specified overdraft listener to receive overdraft events from this Account.
public void deposit(long amount)
    Deposits the passed amount into the Account.
public long getBalance()
    Gets the current balance of this Account.
public long getOverdraft()
    Returns the current overdraft, the amount the bank has loaned to the client that has not yet been repaid.
public long getOverdraftMax()
    Returns the overdraft maximum, the maximum amount the bank will allow the client to owe it.
public void removeOverdraftListener(OverdraftListener l)
    Removes the specified overdraft listener so that it no longer receives overdraft events from this Account.
public long withdraw(long amount) throws InsufficientFundsException
    Withdraws the passed amount from this Account.

The appropriate behavior to take when insufficient funds exist to make a withdrawal depends on the context in which the withdraw method is invoked. Some clients may wish to abort the withdrawal. Some clients may wish to abort the withdrawal and charge a fee. Some clients may wish to abort the withdrawal, charge a fee, and freeze the account. Because the designer of the withdraw method does not know the appropriate behavior, it makes sense to create a messenger and send it to code that does know. The withdraw method creates an InsufficientFundsException and throws the information up the call stack to code written by a programmer with sufficient knowledge of the context to know the appropriate action to take.

Events

Another example of messengers is events. Like exceptions, event objects usually contain a small amount of data, which is passed to the constructor, and some accesor methods by which recipients access the data. Also like exceptions, events usually have short lives. When an event occurs, an event object is instantiated and filled with data that describes the event. The event object is then passed to all listeners that have registered interest in the event. Each listener handles the event in its own way, and the event object is discarded. Diagram 7-3 shows an example of an event class.

Diagram 7-3. The OverdraftEvent class

com.artima.examples.account.ex3
OverdraftEvent
public class OverdraftEvent extends java.util.EventObject
    Event that indicates an overdraft has either been loaned to a client or repaid by a client during a withdrawal or deposit transaction on an Account.
Constructors
public OverdraftEvent(OverdraftAccount source, long overdraft, long amount)
    Constructs an OverdraftEvent with the passed source, and overdraft.
Methods
public long getAmount()
    Returns the amount of money either loaned to the client or repaid to the bank during the transaction that caused this event to be propagated.
public long getOverdraft()
    Returns the current overdraft, the amount of of overdraft after the transaction that caused this event to be propagated.

OverdraftEvents are fired by OverdraftAccounts, shown in Diagram 7-2. An OverdraftAccount represents a bank account with overdraft protection. If a customer attempts to withdraw from his OverdraftAccount more the current balance (an overdraft), the bank will loan the customer enough money to cover the overdraft up to a certain maximum. When a customer with a current overdraft deposits money back into the account, that money will first be used to pay back the bank for its overdraft loan. Any remainder will be deposited into the customer's account. An OverdraftAccount fires an OverdraftEvent if an overdraft occurs on that account, or if an existing overdraft is partially or fully repaid.

OverdraftAccount has an addOverdraftListener method, which accepts an OverdraftListener, shown in Diagram 7-4. OverdraftListeners passed to the addOverdraftListener method receive any OverdraftEvents fired by the OverdraftAccount, until unregistered via the removeOverdraftListener method.

Diagram 7-4. The OverdraftListener interface

com.artima.examples.account.ex3
OverdraftListener
public interface OverdraftListener
    Listener interface for receiving overdraft events.
Methods
public void overdraftOccurred(OverdraftEvent e)
    Invoked when an overdraft has occurred.
public void overdraftRepaid(OverdraftEvent e)
    Invoked when some or all of the outstanding overdraft that a bank has loaned to a client is repaid.

An OverdraftEvent is a messenger. It contains a source, a reference to the object that fired the event, the amount loaned or repaid, and the current overdraft. An OverdraftAccount passes this data to the constructor, then fires the event to the listeners. The listeners can retrieve the data from accessor methods.

The reason a bundle of data makes sense in the OverdraftEvent case is that the designer of class OverdraftAccount doesn't necessarily know everything to do when an overdraft occurs or is repaid. The OverdraftAccount object does know to update the account balance and overdraft amounts. But it is reasonable to expect that other behavior will be desired, such as adding an entry to an audit trail, calculating statistics, or charging a fee based on some complex and often-changing formula. By using an event, other kinds of behavior can be added later and dynamically changed at run time.

Other Examples of Messengers

Messengers often appear in APIs as the types of parameters and return values. For example, collections such as arrays, Lists, and Sets are often used as messengers to pass multiple pieces of data to or from methods or constructors. Occasionally, messenger classes appear in APIs whose instances are intended to be used solely to pass specific data in parameters or return values.

Another place that messengers appear in APIs is to transfer information from one node of a distributed system to another. In the J2EE world such messengers are called transfer objects, which are serializable objects that contain business data. If a client needs several pieces of data from a business object, invoking a separate remote method to get each piece of data is generally less efficient than invoking a single method that returns all needed data. Transfer objects enable clients to receive a collection of needed business data from a business object as the return value of a single remote method call.

A specific example of a messenger used to transmit information across a network is class Locales from the ServiceUI API. As shown in Diagram 7-5, class Locales describes the locales supported by a service UI associated with a Jini service. In the ServiceUI architecture, services describe UIs with data such as Locales objects. Clients inspect the description information and select a best-fit service UI based on the client's capabilities and its user's preferences. Messengers are used to describe service UIs, because although information about UIs is known to UI providers, the behavior of selecting a best-fit UI exists at the client. Thus, UI providers use messengers such as Locales to send UI description data across the network to clients that know how to use that information to select a best-fit UI.

Diagram 7-5. The Locales class

net.jini.lookup.ui.attribute
Locales
public class Locales implements java.io.Serializable
    UI attribute that lists the locales supported by a generated UI.
Constructors
public Locales(java.util.Set locales)
    Constructs a Locales using the passed Set.
Methods
public boolean equals(Object o)
    Compares the specified object (the Object passed in o) with this Locales object for equality.
public java.util.Locale getFirstSupportedLocale(java.util.List locales)
    Iterates through the passed List of Locales and returns the first Locale that is supported by the UI (as defined by isLocaleSupported()), or null, if none of the Locales in the passed array are supported by the UI.
public java.util.Locale getFirstSupportedLocale(java.util.Locale locales)
    Looks through the passed array of Locales (in the order they appear in the array) and returns the first Locale that is supported by the UI (as defined by isLocaleSupported()), or null, if none of the Locales in the passed array are supported by the UI.
public java.util.Set getLocales()
    Returns an unmodifiable java.util.Set that contains java.util.Locale objects, one for each locale supported by the UI generated by the UI factory stored in the marshalled object of the same UIDescriptor.
public int hashCode()
    Returns the hash code value for this Locales object.
public boolean isLocaleSupported(java.util.Locale locale)
    Indicates whether or not a locale is supported by the UI generated by the UI factory stored in the marshalled object of the same UIDescriptor.
public java.util.Iterator iterator()
    Returns an iterator over the set of java.util.Locale objects, one for each locale supported by the UI generated by the UI factory stored in the marshalled object of the same UIDescriptor.

Appropriate Use of Messengers

In parting, I want to warn you to be suspicious of messengers when they appear in your designs. Challenge their existence. Why? Because in my experience a common problem I encounter in object-oriented design reviews is data-oriented design. When you design the tables of a relational database, you should do data-oriented design. When you design the structure of XML documents, you should do data-oriented design. When you design an object-oriented API, however, you should be doing service-oriented design in which data-oriented objects, such as messengers, are special cases.

A messenger makes sense when you don't know the behavior that's appropriate for some particular piece of data. If you do know the appropriate behavior for a messenger's data, then you should refactor. You should move the appropriate code to the data, which will transform the messenger into a more service-oriented object.

Another way to think of this is in terms of division of responsibilities. If you don't believe a particular class that knows of certain information should be responsible for dealing with that information, you can send a messenger containing the information to another class. In general, you should strive to move code to data at design time. But sometimes, you need to use messengers to move data to code at run time.

Messengers are usually immutable, but not always. An example of a mutable messenger is java.awt.event.MouseEvent, whose translatePoint method transforms the x and y positions contained in the MouseEvent by adding passed horizontal and vertical offsets. Immutables are described in Guideline 9. Exceptions are discussed in the Chapter on Exceptions. Events are discussed in the Guideline ? (about the Event Generator idiom).

People-Oriented API Design | Contents | Book List | Print | Email | Screen Friendly Version | Previous | Next

Sponsored Links
Download Artima SuiteRunner Now - It's FREE!

Last Updated: Sunday, May 11, 2003
Copyright © 1996-2003 Artima Software, Inc. All Rights Reserved.
URL: http://www.artima.com/objectdesign/objectP.html
Artima.com is created by Bill Venners