An event trigger and handling scheme is used to determine state transitions. Two possible events can happen to an Account
object: Its balance can change and its state can change. The setter for the balance sets the _balance
instance variable, then triggers the BALANCE_CHANGED event. Similarly, the setter for the state sets the _state
instance variable and then triggers the STATE_CHANGED event. In the event(event)
method in Listing Twelve, different behaviors and state transitions can occur as the result of an event. If the balance has changed, then the current state may be asked some questions. For example, the state is asked if a transaction fee is required. If so, then the transaction fee is deducted from the balance (the instance variable is directly modified; otherwise, another BALANCE_CHANGED event would be triggered) and users are notified. Next, the state transition rules come into effect. Depending on the current balance and the current state's responses to various checker methods, the account changes to a new state. A STATE_CHANGED event is handled by asking the state object if the account is overdrawn. If so, users are notified of this. This will only occur when the account's state changes to OverdrawnState,
which is the desired behavior.
Listing Twelve
/**************** (Account methods) ***************/ #include "account.h" const double CURRENT_RATE = 0.065; const double CURRENT_MIN_BALANCE = 500.00; const double CURRENT_TRANSACTION_FEE = 1.00; double Account::CurrentRate() { /* CODE THAT OBTAINS CURRENT RATE FROM SOME DATABASE */ return CURRENT_RATE; } double Account::CurrentMinimumBalance() { /* CODE THAT OBTAINS CURRENT MIN BALANCE FROM SOME DATABASE */ return CURRENT_MIN_BALANCE; } double Account::CurrentTransactionFee() { /* CODE THAT OBTAINS CURRENT FEE FROM SOME DATABASE */ return CURRENT_TRANSACTION_FEE; } void Account::event(EVENT event) { switch (event) { case BALANCE_CHANGED : if (this->state()->requiresTransactionFee()) { /* Set the instance variable directly because*/ /* we don't want to trigger another event. */ _balance -= Account::CurrentTransactionFee(); cout << "A TRANSACTION FEE WAS CHARGED" << endl; } if (this->balance() < 0) { if (this->state()->isNotOverdrawn()) this->changeState(new OverdrawnState()); } else if (this->balance() < Account::CurrentMinimumBalance()) { if (this->state()->isInterestBearing() || this->state()->isOverdrawn()) this->changeState(new NonInterestBearingState()); } else { if (this->state()->isNotInterestBearing()) this->changeState(new InterestBearingState()); } break; case STATE_CHANGED : if (this->state()->isOverdrawn()) this->sendNoticeToAccountHolder(); break; } }
Once again, refer to Listing Eight for the ATM main program. Executing this code using the check implementation will result in the same behavior and output as for the delegation implementation.
State as Singleton
Performance overhead can be a concern for both implementations. If a context object can go through many state transitions, then a run-time overhead will be paid because of the construction and destruction of state
objects. One solution is to make the various state
classes Singletons. In the checker implementation, this is simple because none of the state
classes contain instance variables. In the delegation implementation, Account
would have to keep its own _balance
instance variable. State
would have no instance variables. Each method that the state hierarchy has would require an extra parameter, where the context
object could pass itself in by reference (deposit(amount, context)
, withdraw(amount, context)
, and payInterest(c
ontext), for instance); thereby eliminating the need for the _context
instance variable.
Conclusion
Both approaches to the State design patterns have their pros and cons. In the delegation implementation, the state class hierarchy contains and implements the state-dependent attributes and methods of its context
class. This usually results in the context
class being very thin. The state hierarchy also contains and implements the rules for any state transitions. The checker implementation instead keeps the bulk of the code in the context
class.
The delegation implementation results in slightly more code to maintain but, in terms of complexity, it is a matter of opinion which is more complex. The checker implementation keeps all the code for any one method in one place, thereby allowing a programmer to concisely see all the different code paths for that method. The delegation implementation, on the other hand, spreads code across multiple classes. Although it is more difficult to see all the various code paths, it makes it easier to understand each individual code path because they aren't cluttered by large groups of if-then-else
constructs. My recommendation is to use the checker implementation for classes that contain a small set of states. If you design classes that can go through many different possible states and, therefore, could have many different state-dependent behaviors, the delegation implementation is probably the better way to go.
Julian develops Java applications to support securities trading. He can be contacted at [email protected].