Sponsored Link •
|
Advertisement
|
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 String
s 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.
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
|