The Artima Developer Community
Sponsored Link

Guideline 3
Interface Design by Bill Venners
Design Service-Oriented Objects that use their state to decide how to behave

Advertisement

Interface Design | Contents | Previous | Next

When I was first struggling to understand object-oriented programming, I happened to leaf through a copy of Object-Oriented Design with Applications by Grady Booch. In this book I found a sentence that gave my my first real insight into what an object is. Booch said, simply:

An object has state, behavior, and identity.

A diagram accompanying this sentence showed three images of a hammer object. (What did the state picture show?) The behavior picture showed the hammer pounding away at a nail. The identity picture showed one hammer sticking its head up among a sea of hammers.

In the Java virtual machine, an object's state is values of its instance variables. Its behavior is the actions it takes when you invoke its instance methods. Its identity is the address of the object image on the heap, which is available to programmers as its reference. If two object's have identical state and behavior, the way you can tell them apart is by comparing their references.

With time and practical 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 have encountered have had both interesting state and interesting behavior, as predicted by Booch's statement. I call this most common kind of 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. On the other hand, I have on occasion encountered objects that have little or no interesting state. These objects, dubbed Flyweights by the Design Patterns book, are composed primarily of behavior. Lastly, some objects are immutable, which means that once their state is established at the beginning of their lifetimes, the state never changes. Although every object does indeed have 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.

With experience I came to realize is that there's really a spectrum between state and behavior, and that every object design lands somewhere on the state-behavior 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 Objects, which have both state and behavior. Fewer object designs are Messengers, which show up at the state end of the spectrum. Still fewer object designs are Flyweights, which show up at the behavior end of the spectrum. 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, therefore, is the Service-Oriented Object. Such an object has state, stored in private instance variables, and behavior, represented by the code of its instance methods. You can ask a service-oriented object to do something for you, to provide a service to you, by invoking one of its methods. The method provides the service by taking actions, possibly changing the object's state, and returning.

Perhaps here need a paragraph on the contract. What the contract is and that it is expressed in behavior terms, and that state does not appear in the contract so much as it is used by the object to decide how to behave. Word count is currently 1275, so I have room.

The day I was trying to think of an example of a service-oriented object for this book, I visited the post office and bought stamps from a vending machine. I decided to use as an example an object that models the behavior of an extremely simple stamp machine. Such an object could be used in the control software of an actual physical incarnation of the stamp machine. Here's a specification of requirements for such a machine:

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. An LED display on the stamp dispenser indicates to the user the total amount of money that has been inserted so far. As soon as 20 or more cents has been inserted, a 20 cent stamp is automatically dispensed and any change is returned. The only amounts that show up on the display, therefore, are 0, 5, 10, and 15 cents. If a dime and a nickel have been inserted, the display will indicate 15 cents. If the user then inserts another dime, the stamp dispenser will dispense a 20 cent stamp, return a nickel, and change the display to show 0 cents. In addition to a slot for inserting coins, a slot for dispensing stamps and returning coins, 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 the display changes to show 0 cents.

An object-oriented solution to these requirements could include a class named StampDispenser, that models the functionality of the real-world stamp dispenser, as shown in Listing 3-1:

Listing 3-1. A stamp dispenser.

  1 package com.artima.examples.stampdispenser.ex1;
  2
  3 import java.util.Set;
  4 import java.util.Iterator;
  5 import java.util.HashSet;
  6
  7 /**
  8 * A stamp dispenser that accepts nickels and dimes and dispenses
  9 * twenty cent stamps.
 10 *
 11 * @author Bill Venners
 12 */
 13 public class StampDispenser {
 14
 15     private final static int STAMP_VALUE = 20;
 16     private int balance;
 17     private Set listeners = new HashSet();
 18
 19     /**
 20     * Constructs a new stamp dispenser with a starting current value of zero.
 21     */
 22     public StampDispenser() {
 23     }
 24
 25     /**
 26     * Adds the specified stamp dispenser listener to receive stamp dispenser events
 27     * from this stamp dispenser. If <code>l</code> is <code>null</code>, no exception
 28     * is thrown and no action is performed. If <code>l</code> is already registered
 29     * as a listener, no action is performed.
 30     */
 31     public synchronized void addStampDispenserListener(StampDispenserListener l) {
 32
 33         listeners.add(l);
 34     }
 35
 36     /**
 37     * Removes the specified stamp dispenser listener so that it no longer
 38     * receives stamp dispenser events from this stamp dispenser. This method
 39     * performs no function, nor does it throw an exception, if the listener
 40     * specified by the argument was not previously added to this
 41     * component. If <code>l</code> is <code>null</code>, no exception is
 42     * thrown and no action is performed.
 43     */
 44     public synchronized void removeStampDispenserListener(StampDispenserListener l) {
 45
 46         listeners.remove(l);
 47     }
 48
 49     /**
 50     * Add either 5 or 10 cents to the stamp dispenser. If the amount added
 51     * causes the current value to become or exceed 20 cents, the price of
 52     * a stamp, the stamp will be automatically dispensed. If the stamp is
 53     * dispensed, the amount of the current value after the stamp is dispensed
 54     * is returned to the client.
 55     *
 56     * @throws IllegalArgumentException if passed <code>amount</code> doesn't
 57     *    equal either 5 or 10
 58     */
 59     public synchronized void add(int amount) {
 60
 61         if ((amount != 5) && (amount != 10)) {
 62             throw new IllegalArgumentException();
 63         }
 64
 65         balance += amount;
 66
 67         if (balance >= STAMP_VALUE) {
 68
 69             // Dispense a stamp and return any change
 70             // balance - STAMP_VALUE is amount in excess of twenty cents
 71             // (the stamp price) to return as change. After dispensing the stamp and
 72             // returning any change, the new balance will be zero.
 73             StampDispenserEvent event = new StampDispenserEvent(this, balance - STAMP_VALUE, 0);
 74             balance = 0;
 75             fireStampDispensed(event, listeners);
 76         }
 77         else {
 78
 79             // Fire an event to indicate the balance has increased
 80             StampDispenserEvent event = new StampDispenserEvent(this, amount, balance);
 81             fireCoinAccepted(event, listeners);
 82         }
 83     }
 84
 85     /**
 86     * Returns coins. If the current value is zero, no action is
 87     * performed.
 88     */
 89     public synchronized void returnCoins() {
 90
 91         // Make sure balance is greater than zero, because no event should
 92         // be fired if the coin return lever is pressed when the stamp
 93         // dispenser has a zero balance
 94         if (balance > 0) {
 95
 96             // Return the entire balance to the client
 97             StampDispenserEvent event = new StampDispenserEvent(this, balance, 0);
 98             balance = 0;
 99             fireCoinsReturned(event, listeners);
100         }
101     }
102
103     /**
104     * Helper method that fires coinAccepted events.
105     */
106     private static void fireCoinAccepted(StampDispenserEvent event,
107         Set listeners) {
108
109         Iterator it = listeners.iterator();
110         while (it.hasNext()) {
111             StampDispenserListener l = (StampDispenserListener) it.next();
112             l.coinAccepted(event);
113         }
114     }
115
116     /**
117     * Helper method that fires stampDispensed events.
118     */
119     private static void fireStampDispensed(StampDispenserEvent event,
120         Set listeners) {
121
122         Iterator it = listeners.iterator();
123         while (it.hasNext()) {
124             StampDispenserListener l = (StampDispenserListener) it.next();
125             l.stampDispensed(event);
126         }
127     }
128
129     /**
130     * Helper method that fires coinsReturned events.
131     */
132     private static void fireCoinsReturned(StampDispenserEvent event,
133         Set listeners) {
134
135         Iterator it = listeners.iterator();
136         while (it.hasNext()) {
137             StampDispenserListener l = (StampDispenserListener) it.next();
138             l.coinsReturned(event);
139         }
140     }
141 }

The StampDispenser offers its primary services to clients via two public methods: add and returnCoins. The StampDispenser also enables clients to be notified of important events by registering themselves via the addStampDispenserListener method. Clients can change their minds via the removeStampDispenser method. Here's the StampDispenserListener interface:

Listing 3-2. Stamp dispenser listener.

 1 package com.artima.examples.stampdispenser.ex1;
 2
 3 /**
 4 * Listener interface for receiving stamp dispenser events.
 5 */
 6 public interface StampDispenserListener {
 7
 8     /**
 9     * Invoked when a stamp has been dispensed. If coins
10     * have also been returned as change, the amount is
11     * indicated by the return value of <CODE>getReturnedAmount</CODE>
12     * method of the passed <code>StampDispenserEvent</code>.
13     */
14     void stampDispensed(StampDispenserEvent e);
15
16     /**
17     * Invoked when coins have been returned as the result of
18     * the <code>returnCoins</code> method being invoked on
19     * a <code>StampDispenser</code>. Coins that are returned
20     * as change when a stamp is dispensed are reported via
21     * the event passed to <code>stampDispensed</code>.
22     */
23     void coinsReturned(StampDispenserEvent e);
24
25     /**
26     * Invoked when coins have been accepted but no stamp
27     * has been dispensed. A coin that causes a stamp to
28     * be dispensed does not generate a <code>coinsAccepted</code>
29     * method invocation, just a <code>stampDispensed</code>
30     * method invocation.
31     */
32     void coinAccepted(StampDispenserEvent e);
33 }

Were an instance of StampDispenser to be used to control a real life simple stamp dispenser, the listeners would be responsible for actually returning coins, dispensing a stamp, and changing the display that shows how much money has been inserted so far. The client that invoked the add method would be code that knows that money was inserted into the real slot. The client that invoked the returnCoins method would be code that knows the return coins lever was pressed.

When a listener is notified, it receives an instance of StampDispenserEvent:

Listing 3-3. Stamp dispenser event.

 1 package com.artima.examples.stampdispenser.ex1;
 2
 3 import java.util.EventObject;
 4
 5 /**
 6 * Event that indicates a stamp dispenser has performed
 7 * an action. The three kinds of actions that cause a
 8 * stamp dispenser event to be propagated are:
 9 * (1) accepting a coin, (2) dispensing a stamp,
10 * (3) returning coins.
11 */
12 public class StampDispenserEvent extends java.util.EventObject {
13
14     private int amountReturned;
15     private int balance;
16
17     /**
18     * Constructs a <code>StampDispenserEvent</code> with
19     * <code>amountReturned</code>, and <code>balance</code>.
20     *
21     * @param amountReturned the amount of money, if any,
22     *     returned to the client, either as the result of
23     *     a coin return or as change when dispensing a stamp.
24     * @param balance the amount of money, if any, remaining
25     *     as the current balance of the stamp dispenser after
26     *     this event has occurred.
27     * @throws IllegalArgumentException if balance is not one
28     *     of 0, 5, 10, or 15; or if amountReturned is not one.
29     *     of 0, 5, 10, or 15.
30     */
31     public StampDispenserEvent(StampDispenser source, int amountReturned,
32         int balance) {
33
34         super(source);
35
36         if (balance != 0 && balance != 5 && balance != 10
37             && balance != 15) {
38
39             throw new IllegalArgumentException();
40         }
41
42         if (amountReturned != 0 && amountReturned != 5 && amountReturned != 10
43             && amountReturned != 15) {
44
45             throw new IllegalArgumentException();
46         }
47
48         this.amountReturned = amountReturned;
49         this.balance = balance;
50     }
51
52     /**
53     * Returns the amount of money returned to the client,
54     * expressed in units of American pennies.
55     */
56     public int getAmountReturned() {
57         return amountReturned;
58     }
59
60     /**
61     * Returns the current balance: the amount of money that has been inserted
62     * into the stamp dispenser, but not returned via a coin return
63     * or consumed in exchange for a dispensed stamp. For example,
64     * if the <code>balance</code> is zero and a nickel is
65     * added, the <code>balance</code> in the resulting
66     * stamp dispenser event will be 5. If another dime is added,
67     * the <code>balance</code> in the resulting stamp dispenser
68     * event will be 15. If the <code>returnCoins</code> method is then
69     * invoked on the stamp dispenser, the <code>balance</code>
70     * of the resulting stamp dispenser event will be 0.
71     */
72     public int getBalance() {
73         return balance;
74     }
75 }

A StampDispenserEvent contains information the listener can use to respond to the event. For example, listeners can know how much money to return, if any, and what amount should show up on the display.

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. Is is called "service-oriented," because its contract with the client is expressed in terms of services offered, which is behavior. It's contract is not expressed in terms of data, which is state.

Service-oriented objects like StampDispenser have state, but they use their state to decide how to behave when their methods are invoked. When the StampDispenser's add method is invoked, for example, 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. Similarly, the returnCoins method uses balance to decide whether or not to return any money, and if so how much money to return.

The main point to take away from this guideline 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.


Sponsored Links



Google
  Web Artima.com   
Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use