In object-oriented design, objects can modify their behavior based on the current state of their attributes. The State pattern (see Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, et al., Addison-Wesley, 1994) abstracts the ability for an object to change its behavior. For any class that has state-dependent behavior, a separate class hierarchy can be created representing the various states that the subject class can have.
In this article, I present two ways the State design pattern can be implemented in C++:
- A delegation model, where the context object (the object that has state-dependent behavior) delegates the handling of a state-dependent message to its state object. The state object performs the appropriate action and changes the context's state, if necessary. This delegation implementation is the suggested implementation in Design Patterns.
- The checker implementation, where all behavior is kept in the context class, but the state class hierarchy offers a set of checker methods that respond true or false based on the current state. Based on these checker methods, the context object performs the appropriate action.
To illustrate how you can use these two implementations, I present an automatic-teller machine (ATM) program that lets users deposit money into an account, withdraw money from it, or ask the account to pay interest (6.5 percent in this example). The rules for an account are such that it bears interest only if the balance is greater than or equal to some minimum balance ($500.00, in this case). Also, if the balance is less than the minimum balance, then all deposits and withdrawals will result in a transaction fee being charged against the account (the transaction fee is $1.00). One unique feature of this account is that it allows the account holder to overdraw on it; thereby allowing the balance to go negative. Once it is negative, the account holder is not allowed to withdraw any more until enough deposits are made to make the balance positive again.
The Delegation Implementation
In Figure 1, which is a static model of the delegation implementation, a relationship exists between an Account
and a State
. State
contains all the attributes of Account
(in this case, the balance). Listing One contains the definition for class State
. Not only does State
contain its context's attributes, but it also has reference to its context, which, in our example, is an Account
object. The State
class also provides the deposit(amount)
and withdraw(amount)
methods because they do have specific abstract behavior (see Listing Two). Both of these methods adjust the balance accordingly, but then they call the transitionState()
function, which is specified as a pure virtual function. All derived classes must implement this function. This is where the derived classes will contain their specific rules for determining when an account transitions from one state to another, and cause the state transition to occur.
Figure 1: The delegation implementation.
Listing One
/***** (State class definition) ****************/ class Account; class State { friend class Account; public: static State * InitialState(Account * account); static double CurrentMinimumBalance() { /* CODE THAT OBTAINS CURRENT MIN BALANCE FROM SOME DATABASE */ return CURRENT_MIN_BALANCE; }; private: Account * _context; double _balance; protected: Account * context() const {return _context;}; void context(Account * newAccount) {_context = newAccount;}; double balance() const {return _balance;}; void balance(double newBalance) {_balance = newBalance;}; virtual State * transitionState() = 0; public: virtual void deposit(double amount); virtual void withdraw(double amount); virtual void payInterest() = 0; public: State(Account * account, double balance) : _context(account), _balance(balance) {}; State(const State * source) : _context(source->context()), _balance(source->balance()) {}; };
Listing Two
/************** (State class method code) ***************/ /************ class State **********************/ State * State::InitialState(Account * account) { return new NonInterestBearingState(account, 0.0); } void State::deposit(double amount) { this->balance(this->balance() + amount); this->transitionState(); } void State::withdraw(double amount) { this->balance(this->balance() - amount); this->transitionState(); }
The State
class also specifies the payInterest()
method as a pure virtual function because the function of paying interest is very specific to the concrete class.
State
, which is an abstract class, has three classes derived from it. The following classes (see Listing Three) represent the three states that an Account
can have: InterestBearingState
, NonInterestBearingState
, and OverdrawnState
. InterestBearingState
inherits the deposit(amount)
and withdraw(amount)
methods. Listing Four is an implementation of the payInterest()
function as well as the transitionState()
function (both specified as pure virtual functions in class State
). transitionState()
determines what the new state should be, if a state change is necessary, and tells its context (the account) to change state.
Listing Three
/*************** (Concrete state classes) *************/ class InterestBearingState : public State { public: static double CurrentRate() { /* CODE THAT OBTAINS CURRENT RATE FROM SOME DATABASE */ return CURRENT_RATE; }; protected: virtual State * transitionState(); public: virtual void payInterest(); public: InterestBearingState(Account * account, double balance) : State(account, balance) {}; InterestBearingState(const State * source) : State(source) {}; }; class NonInterestBearingState : public State { public: static double CurrentTransactionFee() { /* CODE THAT OBTAINS CURRENT TRANSACTION FEE FROM SOME DATABASE */ return CURRENT_TRANSACTION_FEE; }; protected: virtual State * transitionState(); public: virtual void deposit(double amount); virtual void withdraw(double amount); virtual void payInterest(); public: NonInterestBearingState(Account * account, double balance) : State(account, balance) {}; NonInterestBearingState(const State * source) : State(source) {}; }; class OverdrawnState : public NonInterestBearingState { protected: void sendNoticeToAccountHolder() { /* PRINT OUT AND MAIL A NOTICE INDICATING ACCOUNT OVERDRAWN */ cout << "YOUR ACCOUNT IS OVERDRAWN" << endl; }; protected: virtual State * transitionState(); public: virtual void withdraw(double amount); public: OverdrawnState(Account * account, double balance) : NonInterestBearingState(account, balance) { this->sendNoticeToAccountHolder(); }; OverdrawnState(const State * source) : NonInterestBearingState(source) { this->sendNoticeToAccountHolder(); }; };
Listing Four
/*************** (InterestBearingState methods) ********************/ State * InterestBearingState::transitionState() { if (this->context()->balance() < 0) this->context()->changeState(new OverdrawnState(this)); else if (this->context()->balance() < State::CurrentMinimumBalance()) this->context()->changeState(new NonInterestBearingState(this)); return this->context()->state(); } void InterestBearingState::payInterest() { this->balance(this->balance() * (1 + this->CurrentRate())); this->transitionState(); }
The NonInterestBearingState
class definition (see Listing Three) overrides the deposit(amount)
and withdraw(amount)
methods. Listing Five shows that both methods first deduct a transaction fee from the balance, then invoke the respective deposit(amount)
or withdraw(amount)
function in the base class State
. They both also tell the user that a transaction fee was charged.
Listing Five
/****************** (NonInterestBearingState methods) *******************/ State * NonInterestBearingState::transitionState() { if (this->context()->balance() < 0) this->context()->changeState(new OverdrawnState(this)); else if (this->context()->balance() >= State::CurrentMinimumBalance()) this->context()->changeState(new InterestBearingState(this)); return this->context()->state(); } void NonInterestBearingState::payInterest() { /* PAY NO INTEREST AT ALL */ cout << "THIS ACCOUNT IS CURRENTLY NOT EARNING INTEREST" << endl; } void NonInterestBearingState::deposit(double amount) { /* Charge the transaction fee and then deposit */ this->balance(this->balance() - this->CurrentTransactionFee()); this->State::deposit(amount); cout << "A TRANSACTION FEE WAS CHARGED" << endl; } void NonInterestBearingState::withdraw(double amount) { /* Charge the transaction fee and then withdraw */ this->balance(this->balance() - this->CurrentTransactionFee()); this->State::withdraw(amount); cout << "A TRANSACTION FEE WAS CHARGED" << endl; }
OverdrawnState
is derived from NonInterestBearingState
because it is a special case of a noninterest-bearing account. Like a noninterest-bearing account, an overdrawn account does not pay interest, it charges a transaction fee and allows deposits to be made. The only exception, or specialization, is that overdrawn accounts do not allow withdrawals to be made. In this example, OverdrawnState
overrides the withdraw(amount)
method, disallowing the withdrawal and instead telling users that the withdrawal is not permitted (see Listing Six).
Listing Six
/***************** (OverdrawnState methods) **************/ State * OverdrawnState::transitionState() { if (this->context()->balance() >= State::CurrentMinimumBalance()) this->context()->changeState(new InterestBearingState(this)); else if (this->context()->balance() >= 0) this->context()->changeState(new NonInterestBearingState(this)); return this->context()->state(); } void OverdrawnState::withdraw(double amount) { /* DO NOT ALLOW THEM TO WITHDRAW MORE MONEY */ cout << "YOU ARE NOT ALLOWED TO WITHDRAW FROM AN OVERDRAWN ACCOUNT" << endl; }
NonInterestBearingState
and OverdrawnState
both provide implementations for the transitionState()
function (see Listings Five and Six, respectively).