People-Oriented API Design |
Contents |
Book List |
Print |
Email |
Screen Friendly Version |
Previous |
Next
|
Object-oriented APIs are toolboxes for programmers.
An API contains types and their members: classes to instantiate,
classes to subclass, interface
s 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.
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.
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
Lastly, objects help programmers create systems
that, despite being complex, are
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 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
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.
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.
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.
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:
HAS_0
, HAS_5
,
HAS_10
, HAS_15
add5
, add10
, returnCoins
dispenseStamp
, ret5
,
ret10
, ret15
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. The stamp dispenser state-transition diagram
In 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.
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. 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:
HAS_0
state: balance
== 0
HAS_5
state: balance == 5
HAS_10
state: balance == 10
HAS_15
state: balance == 15
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:
sending add5
message:
stampDispenser.add(5)
add10
message: stampDispenser.add(10)
sending returnCoins
message: stampDispenser.returnCoins()
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 ?.
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
Consider the Matrix
class shown
in 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
int s.
|
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 .
|
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
Now consider 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 int s.
|
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
int s.
|
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 Matrix
class's add
method, shown
in
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 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 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.
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 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.
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
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.
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.
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
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. 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. 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. |
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. 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 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.
In
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
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. The InsufficientFundsException
is
a messenger
com.artima.examples.account.ex3 InsufficientFundsException
|
public class InsufficientFundsException extends Exception Exception thrown by Account s 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 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.
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. 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. |
OverdraftEvent
s are fired by OverdraftAccount
s,
shown in 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
OverdraftListener
s passed to the addOverdraftListener method
receive any OverdraftEvent
s 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.
Messengers often appear in APIs as the types
of parameters and return values. For example, collections such as
arrays, List
s, and Set
s 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 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 Locale s
and returns the first Locale that is
supported by the UI (as defined by isLocaleSupported() ),
or null , if none of the Locale s in
the passed array are supported by the UI.
|
public java.util.Locale getFirstSupportedLocale(java.util.Locale locales) Looks through the passed array of Locale s
(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 Locale s 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 .
|
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
|
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 |