Designing with Patterns Workshop
The Event Generator Idiom
Event Generator Idiom
-
Enable interested objects (listeners) to be notified of a state change or
other events experienced by an "event generator."
-
The JavaBeans/AWT/Swing event model
-
Step 1. Define event category classes
-
Step 2. Define listener interfaces
-
Step 3. Design adapter classes (optional)
-
Step 4. Define event generator classes
Step 1. Define Event Category Classes
-
Define a separate event category class for each major category of events that
may be experienced and propagated by the event generator.
-
Make each event category class extend
java.util.EventObject
.
-
Design each event category class so that it encapsulates the information that
needs to be propagated from the observable to the listeners for that
category of events.
-
Give the event category class a name that ends in
Event
, such as
OverdraftEvent
.
1 package com.artima.examples.account.ex4;
2
3 import java.util.EventObject;
4
5 /**
6 * Event that indicates an overdraft has either been loaned to a client or
7 * repaid by a client during a withdrawal or deposit transaction on an
8 * <code>Account</code>.
9 */
10 public class OverdraftEvent extends java.util.EventObject {
11
12 private long overdraft;
13 private long amount;
14
15 /**
16 * Constructs an <code>OverdraftEvent</code> with the passed
17 * <code>source</code>, and <code>overdraft</code>.
18 *
19 * @param source the source of the event
20 * @param overdraft the current amount of overdraft outstanding in the
21 * <code>OverdraftAccount</code>
22 * @throws IllegalArgumentException if either of <code>overdraft</code>
23 * or <code>amount</code>, passed as parameters to this constructor,
24 * are less than or equal to zero.
25 */
26 public OverdraftEvent(OverdraftAccount source,
27 long overdraft, long amount) {
28
29 super(source);
30
31 if (overdraft <= 0 || amount <= 0) {
32 throw new IllegalArgumentException();
33 }
34
35 this.overdraft = overdraft;
36 this.amount = amount;
37 }
38
39 /**
40 * Returns the current overdraft, the amount of of overdraft after the
41 * transaction that caused this event to be propagated.
42 *
43 * @returns the current overdraft
44 */
45 public long getOverdraft() {
46 return overdraft;
47 }
48
49 /**
50 * Returns the amount of money either loaned to the client or repaid to
51 * the bank during the transaction that caused this event to be
52 * propagated.
53 *
54 * @returns the amount loaned or repaid
55 */
56 public long getAmount() {
57 return amount;
58 }
59 }
Step 2. Define Listener Interfaces
1 package com.artima.examples.account.ex4;
2
3 import java.util.EventListener;
4
5 /**
6 * Listener interface for receiving overdraft events.
7 */
8 public interface OverdraftListener extends EventListener {
9
10 /**
11 * Invoked when an overdraft has occurred. This method will
12 * be invoked on listeners whenever the bank loans money
13 * to a client that has requested a withdrawal of more funds
14 * than is available in the account's balance.
15 */
16 void overdraftOccurred(OverdraftEvent e);
17
18 /**
19 * Invoked when some or all of the outstanding overdraft
20 * that a bank has loaned to a client is repaid.
21 */
22 void overdraftRepaid(OverdraftEvent e);
23 }
Step 3. Define Adapter Classes (Optional)
-
For each listener interface that contains more than one method, define an
adapter class that implements the interface fully with methods that do
nothing.
-
Name the adapter class by replacing
Listener
in the listener
interface name with Adapter
. For example, the adapter class
for OverdraftListener
would be OverdraftAdapter
.
1 package com.artima.examples.account.ex4;
2
3 /**
4 * An abstract adapter class for receiving overdraft events. The methods in
5 * this class are empty. This class exists as convenience for listener
6 * objects.
7 */
8 public abstract class OverdraftAdapter implements OverdraftListener {
9
10 /**
11 * Invoked when an overdraft has occurred. This method will
12 * be invoked on listeners whenever the bank loans money
13 * to a client that has requested a withdrawal of more funds
14 * than is available in the account's balance.
15 *
16 * @param e the overdraft event object
17 */
18 public void overdraftOccurred(OverdraftEvent e) {
19 }
20
21 /**
22 * Invoked when some or all of the outstanding overdraft
23 * that a bank has loaned to a client is repaid.
24 *
25 * @param e the overdraft event object
26 */
27 public void overdraftRepaid(OverdraftEvent e) {
28 }
29 }
Step 4. Define Event Generator Classes
-
For each category of events that will be propagated by instances of this
class, define a pair of listener add/remove methods.
-
Name the add method
add<listener-interface-name>()
and the
remove
method remove<listener-interface-name>()
. For example, the
listener add and remove methods for a OverdraftEvent
would be
named addOverdraftListener()
and
removeOverdraftListener()
.
-
You'll also need to fire the events, but that's implementation.
1 package com.artima.examples.account.ex4;
2
3 /**
4 * Represents a bank account with overdraft protection. Instances of this
5 * class are instantiated with a specified maximum overdraft. If a client
6 * attempts to withdraw more than the current account balance, the bank may
7 * loan the amount in excess of the balance to the client. The overdraft
8 * maximum passed to an <code>OverdraftAccount</code>'s constructor is the
9 * maximum amount the bank will lend to the client in this manner. When a
10 * client makes a deposit, the bank will pay itself back first before
11 * increasing the account's balance.
12 *
13 * <p>
14 * Money is stored in this account in integral units. Clients can use this
15 * account to store any kind of value, such as money or points, etc. The
16 * meaning of the integral units stored in this account is a decision of the
17 * client that instantiates the account. The maximum amount of units that can
18 * be stored as the current balance of an <code>OverdraftAccount</code> is
19 * Long.MAX_VALUE.
20 */
21 public class OverdraftAccount implements Account, OverdraftEventGenerator {
22
23 /**
24 * Helper back-end <code>BasicAccount</code> object
25 */
26 private BasicAccount account = new BasicAccount();
27
28 /**
29 * Helper back-end <code>OverdraftEventManager</code> object
30 */
31 private OverdraftEventManager eventMan
32 = new OverdraftEventManager();
33
34 /**
35 * The maximum amount the bank will loan to the client.
36 */
37 private final long overdraftMax;
38
39 /**
40 * The current amount the bank has loaned to the client which has not yet
41 * been repaid. This value must at all times be greater than or equal to
42 * zero, and less than or equal to <code>overdraftMax</code>. If this
43 * value is greater than zero, then the balance of the helper
44 * <code>BasicAccount</code> (referenced from the <code>account</code>
45 * instance variable) must be exactly zero.
46 */
47 private long overdraft;
48
49 /**
50 * Constructs a new <code>OverdraftAccount</code> with the passed
51 * overdraft maximum.
52 *
53 * @param overdraftMax the maximum amount the bank will loan to the
54 * client
55 */
56 public OverdraftAccount(long overdraftMax) {
57 this.overdraftMax = overdraftMax;
58 }
59
60 /**
61 * Returns the current overdraft, the amount the bank has loaned to the
62 * client that has not yet been repaid.
63 *
64 * @return the current overdraft
65 */
66 public long getOverdraft() {
67 return overdraft;
68 }
69
70 /**
71 * Returns the overdraft maximum, the maximum amount the bank will allow
72 * the client to owe it. For each instance of
73 * <code>OverdraftAccount</code>, the overdraft maximum is constant.
74 *
75 * @return the overdraft maximum
76 */
77 public long getOverdraftMax() {
78 return overdraftMax;
79 }
80
81 /**
82 * Gets the current balance of this <code>OverdraftAccount</code>.
83 *
84 * @return the current balance
85 */
86 public long getBalance() {
87 return account.getBalance();
88 }
89
90 /**
91 * Withdraws the passed amount from this <code>OverdraftAccount</code>.
92 * If the passed amount is less than or equal to the current balance, all
93 * withdrawn funds will be taken from the balance, and the balance will
94 * be decremented by the passed amount. If the passed amount exceeds the
95 * current balance, the bank may loan the client the difference. The bank
96 * will make the loan only if the difference between the passed amount
97 * and the balance (the shortfall) is less than or equal to the available
98 * overdraft. The available overdraft is equal to the current overdraft
99 * (the amount already loaned to the client and not yet repaid),
100 * subtracted from the overdraft maximum, which is passed to the
101 * constructor of any <code>OverdraftAccount</code>.
102 *
103 * <p>
104 * If the passed amount is less than or equal to the current balance, the
105 * <code>withdraw</code> method decrements the current balance by the
106 * passed amount and returns the passed amount. If the passed amount is
107 * greater than the current balance, but the passed amount minus the
108 * current balance is less than or equal to the available overdraft, the
109 * <code>withdraw</code> method sets the current balance to zero, records
110 * the loan, and returns the requested amount. Otherwise, the passed
111 * amount minus the current balance exceeds the available overdraft, so
112 * the <code>withdraw</code> method throws
113 * <code>InsufficientFundsException</code>.
114 *
115 * Subclasses must withdraw at least the passed amount, but may
116 * effectively withdraw more. For example, if a subclass includes a
117 * notion of a withrawal fee, the subclass's implementation of this
118 * method may charge that fee by decrementing it from the account at the
119 * time of withdrawal.
120 *
121 * @param amount amount to withdraw
122 * @return amount withdrawn from the <code>OverdraftAccount</code>
123 * @throws InsufficientFundsException if the
124 * <code>OverdraftAccount</code> contains insufficient funds for the
125 * requested withdrawal
126 * @throws IllegalArgumentException if requested withdrawal amount is
127 * less than or equal to zero.
128 */
129 public long withdraw(long amount) throws InsufficientFundsException {
130
131 if (amount <= 0) {
132 throw new IllegalArgumentException();
133 }
134
135 long bal = account.getBalance();
136 if (bal >= amount) {
137
138 // Balance has sufficient funds, so just take the
139 // money from the balance.
140 return account.withdraw(amount);
141 }
142
143 long shortfall = amount - bal;
144 long extraAvailable = overdraftMax - overdraft;
145
146 if (shortfall > extraAvailable) {
147 throw new InsufficientFundsException(shortfall
148 - extraAvailable);
149 }
150 overdraft += shortfall;
151 account.withdraw(amount - shortfall);
152
153 OverdraftEvent event = new OverdraftEvent(this, overdraft,
154 shortfall);
155 eventMan.fireOverdraftOccurred(event);
156
157 return amount;
158 }
159
160 /**
161 * Deposits the passed amount into the <code>OverdraftAccount</code>. If
162 * the current overdraft is zero, the balance will be increased by the
163 * passed amount. Otherwise, the bank will attempt to pay off the
164 * overdraft first, before increasing the current balance by the amount
165 * remaining after the overdraft is repaid, if any.
166 *
167 * <p>
168 * For example, if the balance is 0, the overdraft is 100, and the
169 * <code>deposit</code> method is invoked with a passed
170 * <code>amount</code> of 50, the bank would use all 50 of those monetary
171 * units to pay down the overdraft. The overdraft would be reduced to 50
172 * and the balance would remain at 0. If subsequently, the client
173 * deposits another 100 units, the bank would use 50 of those units to
174 * pay off the overdraft loan and direct the remaining 50 into the
175 * balance. The new overdraft would be 0 and the new balance would be
176 * 50.
177 *
178 * Subclasses may effectively deposit more or less than the passed amount
179 * into the <code>OverdraftAccount</code>. For example, if a subclass
180 * includes a notion of funds matching, the subclass implementation of
181 * this method may match some or all of the deposited amount at the time
182 * of deposit, effectively increasing the deposited amount. Or, if a
183 * subclass includes the notion of a deposit fee, the subclass's
184 * implementation of this method may charge that fee by decrementing it
185 * from the account at the time of deposit, effectively reducing the
186 * deposited amount.
187 *
188 * @param amount amount to deposit
189 * @throws ArithmeticException if requested deposit would cause the
190 * balance of this <code>OverdraftAccount</code> to exceed
191 * Long.MAX_VALUE.
192 * @throws IllegalArgumentException if requested withdrawal amount is
193 * less than or equal to zero.
194 */
195 public void deposit(long amount) {
196
197 if (amount <= 0) {
198 throw new IllegalArgumentException();
199 }
200
201 if (overdraft > 0) {
202
203 long amountRepaid = 0;
204
205 if (amount < overdraft) {
206 amountRepaid = amount;
207 overdraft -= amount;
208 }
209 else {
210 long diff = amount - overdraft;
211 amountRepaid = diff;
212 overdraft = 0;
213 account.deposit(diff);
214 }
215
216 OverdraftEvent event = new OverdraftEvent(this, overdraft,
217 amountRepaid);
218 eventMan.fireOverdraftRepaid(event);
219 }
220 else {
221 account.deposit(amount);
222 }
223 }
224
225 /**
226 * Adds the specified overdraft listener to receive overdraft events from
227 * this <code>OverdraftAccount</code>. If <code>l</code> is
228 * <code>null</code>, no exception is thrown and no action is performed.
229 * If <code>l</code> is already registered as a listener, no action is
230 * performed.
231 *
232 * @param l the <code>OverdraftEventListener</code> to add
233 */
234 public void addOverdraftListener(OverdraftListener l) {
235
236 eventMan.addOverdraftListener(l);
237 }
238
239 /**
240 * Removes the specified overdraft listener so that it no longer receives
241 * overdraft events from this <code>OverdraftAccount</code>. This method
242 * performs no function, nor does it throw an exception, if the listener
243 * specified by the argument was not previously added to this component.
244 * If <code>l</code> is <code>null</code>, no exception is thrown and no
245 * action is performed.
246 *
247 * @param l the <code>OverdraftEventListener</code> to remove
248 */
249 public void removeOverdraftListener(OverdraftListener l) {
250
251 eventMan.removeOverdraftListener(l);
252 }
253 }
1 package com.artima.examples.account.ex4;
2
3 import java.util.Set;
4 import java.util.Iterator;
5 import java.util.HashSet;
6
7 /**
8 * A class that manages registration and unregistration of
9 * <code>OverdraftListener</code>s and the firing of
10 * <code>OverdraftEvent</code>s.
11 *
12 * @author Bill Venners
13 */
14 class OverdraftEventManager {
15
16 /**
17 * Unsynchronized <code>HashSet</code> to which listeners are added and
18 * removed via the synchronized methods <code>addOverdraftListener</code>
19 * and <code>removeOverdraftListener</code> methods.
20 */
21 private HashSet listeners = new HashSet();
22
23 /**
24 * Clone of the <code>listeners</code> <code>HashSet</code>, which is
25 * used by the <code>fireOverdraftOccurred</code> and
26 * <code>fireOverdraftRepaid</code> methods to propagate events. This
27 * <code>HashSet</code> always contains a most recent snapshot of the
28 * <code>listeners</code> <code>HashSet</code>, but this
29 * <code>HashSet</code> is never modified, only replaced. Because this
30 * <code>HashSet</code> is never modified, the fire methods can iterate
31 * through the set without synchronization. This implementation approach
32 * is geared towards providing optimum performance for the expected run
33 * time usage in which adding and removing listeners happens less
34 * frequently than firing events to those listeners.
35 */
36 private Set listenersClone = new HashSet();
37
38 /**
39 * Constructs a new <code>OverdraftEventManager</code>. The
40 * <code>OverdraftEventManager</code> starts its life with an empty
41 * listeners list.
42 */
43 public OverdraftEventManager() {
44 }
45
46 /**
47 * Adds the specified overdraft listener to receive overdraft events. If
48 * <code>l</code> is <code>null</code>, no exception is thrown and no
49 * action is performed. If <code>l</code> is already registered as a
50 * listener, no action is performed.
51 *
52 * @param l the <code>OverdraftEventListener</code> to add
53 */
54 public synchronized void addOverdraftListener(OverdraftListener l) {
55
56 listeners.add(l);
57 listenersClone = (Set) listeners.clone();
58 }
59
60 /**
61 * Removes the specified overdraft listener so that it no longer receives
62 * overdraft events. This method performs no function, nor does it throw
63 * an exception, if the listener specified by the argument was not
64 * previously added to this component. If <code>l</code> is
65 * <code>null</code>, no exception is thrown and no action is performed.
66 *
67 * @param l the <code>OverdraftEventListener</code> to remove
68 */
69 public synchronized void removeOverdraftListener(OverdraftListener l) {
70
71 listeners.remove(l);
72 listenersClone = (Set) listeners.clone();
73 }
74
75 /**
76 * Fires overdraftOccurred events to registered listeners.
77 *
78 * @param event the <code>OverdraftEvent</code> to propagate
79 */
80 public void fireOverdraftOccurred(OverdraftEvent event) {
81
82 Iterator it = listenersClone.iterator();
83 while (it.hasNext()) {
84 OverdraftListener l = (OverdraftListener) it.next();
85 l.overdraftOccurred(event);
86 }
87 }
88
89 /**
90 * Fires overdraftRepaid events to registered listeners.
91 *
92 * @param event the <code>OverdraftEvent</code> to propagate
93 */
94 public void fireOverdraftRepaid(OverdraftEvent event) {
95
96 Iterator it = listenersClone.iterator();
97 while (it.hasNext()) {
98 OverdraftListener l = (OverdraftListener) it.next();
99 l.overdraftRepaid(event);
100 }
101 }
102 }
Discussion