artima.com ad
API Design | Contents | Book List | Printer Friendly Version | Previous | Next

Chapter 1 of API Design
The Object
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. With this chapter's guidelines, I hope to help you recognize situations in which these object designs 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.

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. This process of encapsulating code with data in classes, combined with a strong separation of interface and implementation, helps programmers 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 2. 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 2-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 2-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 2-1 shows Example1, a client of the data-oriented Matrix. This client adds two matrices and prints the sum to the standard output.

Listing 2-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 2-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 2-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 2-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 2-1. This code now shows up in the Matrix class's add method, shown in Listing 2-2.

Listing 2-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 2-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 2-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 2-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 5.

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 3. Design service-oriented objects that use their state to decide how to behave

When I was first struggling to understand object-oriented programming, I happened to leaf through Grady Booch's Object-Oriented Design with Applications (Addison-Wesley, February 1994). In it was a sentence that gave me my first real insight into what an object is: "An object has state, behavior, and identity."

Accompanying the sentence was a diagram that showed three sketches of a hammer. The first sketch, labeled STATE, showed a hammer with an attached sign that read, "Grade I Hammer - Hicory Handle - Steel Head." The second drawing, labeled BEHAVIOR, showed the same hammer bending over to pound a nail. The third sketch, labeled IDENTITY, showed the hammer standing on a pedestal, rising over a sea of other hammers.

Booch's definition and hammer illustration helped me get started designing objects. With time and experience, however, I realized that although in theory every object has state, behavior, and identity, in practice different object designs use state, behavior, and identity differently. Granted, most object designs I've encountered have both interesting state and interesting behavior, as predicted by Booch's definition. I call this most common object service-oriented. I have often encountered objects, however, that have little or no interesting behavior. These objects, which I call messengers, are composed primarily of state. At the other extreme are objects that have little or no interesting state. These objects, which I call performers, are composed primarily of behavior.

Moreover, some objects are immutable. Once an immutable object's state is established at the beginning of its lifetime, it never changes. Although every object has a unique identity, immutable objects are differentiated more often by value than by identity. For example, two immutable Strings with the value "Hello, world!" do indeed have separate identities -- each one sits at a different address on the heap. But because their values are the same, their identities are irrelevant. It doesn't matter which one you pass to System.out.println.

I came to realize that a spectrum from state to behavior exists, and that every object design lands somewhere on that spectrum. Figure 3-1 shows this state-behavior spectrum stretched out along the x-axis, with the y-axis showing the frequency of object designs. As Figure 3-1 shows, most object designs tend to be service-oriented, which have both state and behavior. Fewer object designs are messengers and performers, which show up at the state and behavior ends of the spectrumm respectively. Both mutable and immutable objects can show up anywhere on this spectrum.

Figure 3-1. The state-behavior spectrum

The basic and most common object design, the service-oriented object, has state, stored in instance variables, and behavior, contained in instance methods. A service-oriented object can be mutable or immutable. You can ask a service-oriented object 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 service-oriented object, 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 3-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 3-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 3-2.

Diagram 3-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 3-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 3-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 Basic Service-Oriented 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 3-1.

Listing 3-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 3-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, service-oriented 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 4. Think of objects as machines

Objects are invisible machines that programmers use as tools. When you design an object, you design a machine for programmers. I feel thinking of objects as machines is helpful, because it encourages you to focus on both functionality and usability when you design objects.

The two main qualities I look for in any machine are reliability and ease of use. If I buy a new cyclometer for my bicycle, for example, I want that cyclometer to work well -- to reliably keep accurate time, distance, and speed records. But I also want it to be easy to use. I want services I request often, such as switching the display between distance, time of day, and speed, to be quick and easy to access. I don't mind if less-used services, such as setting my tire size or the current time, are more difficult to access. But in no case do I want to invest more than a few minutes of my time pressing buttons or consulting the instruction booklet to access any functionality offered by my cyclometer. Similarly, if I instantiate a class in an API, I want the resulting object to work well. I want the object to do what it promises to do, in an efficient manner, every time I ask it. But I also want the object to be easy to use. I want its interface to allow me, with no more than a few minutes searching through the API documentation, to figure out how to make the object perform the desired service.

Usability and Complexity

To a great extent, API design is about taming complexity. Each API should be focused on one area of responsibility. Each area of responsibility involves some inherent level of complexity. Your goal in designing an API is to tame that complexity so that client programmers need not immerse themselves in it. To the extent possible, you want to hide complexity behind APIs that are easy for client programmers to understand. You tame the complexity of an area of responsibility by creating an API that client programmers find easy to learn and use.

The extent to which you can tame complexity depends in part on the nature of your API's area of responsibility. The level of complexity inherent in many areas of responsibility is so great that you could never devise an API which client programmers could use with only a quick glance at the documentation. I wouldn't expect to be able to sit down in an airplane cockpit, for example, and operate that machine without substantial training time. Likewise, I wouldn't expect to be able to use an API dealing with encryption without spending some time learning about cryptology. Taming complexity doesn't mean making everything simple. It means making everything as simple as possible.

The Nature of the Machine

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.

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 a service-oriented 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, you could express the behavior of the stamp dispenser example from Guideline 3 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 4-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 4-1. The stamp dispenser state-transition diagram

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. 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.

Moreover, the state transition diagram of Figure 4-1 does not completely describe the behavior of a StampDispenser. Class StampDispenser also contains a listeners instance variable, a HashSet that can contain zero to many StampDispenserListener objects. StampDispenser instances actually have more than four possible states. Because of listeners, the number of possible states a StampDispenser could have is undefined, basically infinite. This is the case with many mutable objects: they have 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. Like any machine, an object has a user interface. Because object users are always programmers, an object's user interface is its programmer interface, its API. An object's interface in general consists of a set of accessible methods, constructors, and constants. These are the buttons, knobs, and displays that allow programmers to use the object. 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 ?. The topic of taming complexity in API design is a recurring theme throughout this book. But the guidelines of Chapter ? (5, Semantics) in particular should help you create interfaces that programmers will find understandable and useful.

Guideline 5. Use messengers to transmit information

In Guideline 2, I encourage you to think of objects of bundles of services, not bundles of data. In Guideline 3, 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 3-1. In this guideline, I explain why it sometimes makes sense to disregard Guideline 2 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 2. Most of your object designs should be service-oriented objects, as described in Guideline 3. 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 5-1 shows an example of an exception class.

Diagram 5-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 5-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 5-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 OverdraftAccount with the passed overdraft maximum.
Methods
public void addOverdraftListener(OverdraftListener l)
    Adds the specified overdraft listener to receive overdraft events from this OverdraftAccount.
public void deposit(long amount)
    Deposits the passed amount into the OverdraftAccount.
public long getBalance()
    Gets the current balance of this OverdraftAccount.
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 OverdraftAccount.
public long withdraw(long amount) throws InsufficientFundsException
    Withdraws the passed amount from this OverdraftAccount.

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 5-3 shows an example of an event class.

Diagram 5-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 5-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 5-4. OverdraftListeners passed to the addOverdraftListener method receive any OverdraftEvents fired by the OverdraftAccount, until unregistered via the removeOverdraftListener method.

Diagram 5-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 5-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 5-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 the most 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 7. Exceptions are discussed in the Chapter on Exceptions. Events are discussed in the Guideline ? (about the Event Generator idiom).

API Design | Contents | Book List | Printer Friendly Version | Previous | Next

Last Updated: Friday, April 26, 2002
Copyright © 1996-2002 Artima Software, Inc. All Rights Reserved.
URL: http://www.artima.com/apidesign/objectP.html
Artima.com is created by Bill Venners