State Patterns & C++

In object-oriented design, objects can modify their behavior based on the current state of their attributes. State patterns abstract the ability for an object to change its behavior. Julian presents two ways the State design pattern can be implemented in C++.


June 01, 2000
URL:http://www.drdobbs.com/cpp/state-patterns-c/184404132

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++:

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).

How does the account fit into all of this? Listing Seven shows the definition for the Account class. The methods deposit(amount), withdraw(amount), payInterest(), and balance() all delegate their implementations to the account's current state object. Depending on the current state object, these functions will behave differently. As you can see, the Account class is very light in code. Its largest function is the changeState(newState) method which cleanly sets its _state instance variable while deleting the previous state object.

Listing Seven
/**************** (Account class definition) **********/
#include "state.h"
class Account
{
  friend class State;
  friend class InterestBearingState;
  friend class NonInterestBearingState;
  friend class OverdrawnState;

  private:
    State * _state;
  protected:
    State *  state() const {return _state;};
    void     state(State * newState) {_state = newState;};
    void changeState(State * newState)
    {
      if (newState != this->state())
      {
        delete this->state();
        this->state(newState);
      }
    };
  public:
    double balance()
    {
      return this->state()->balance();
    };
    void deposit(double amount)
    {
      this->state()->deposit(amount);
    };
    void withdraw(double amount)
    {
      this->state()->withdraw(amount);
    };
    void payInterest()
    {
      this->state()->payInterest();
    };
  public:
    Account()
      : _state(State::InitialState(this))
    {};
    virtual ~Account()
    {
      delete this->state();
    };
};

Listing Eight is a program that creates an account object and allows a user to make deposits, withdrawals, and to ask for interest payment. Listing Nine is sample output of this program.

Listing Eight
/************** (atm.C) ************/
#include "account.h"
#include "iostream.h"
#include "iomanip.h"

int main()
{
  char   option;
  double amount;
  int    quit = 0;

  Account account;
  cout << "WELCOME TO JULIAN'S BANK" << endl;
  cout << "========================" << endl;

  while (!quit)
  {
    cout << endl << "Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, 
                                                     or (q)uit? " << flush;
    cin >> option;
    cout << setiosflags(ios::fixed) << setprecision(2);
    switch (option)
    {
      case 'd' :
      case 'D' :
           cout << "Deposit amount = " << flush;
           cin >> amount;
           account.deposit(amount);
           cout << "Balance = $" << account.balance() << endl;
           break;
      case 'w' :
      case 'W' :
           cout << "Withdrawal amount = " << flush;
           cin >> amount;
           account.withdraw(amount);
           cout << "Balance = $" << account.balance() << endl;
           break;
      case 'i' :
      case 'I' :
           account.payInterest();
           cout << "Balance = $" << account.balance() << endl;
           break;
      case 'q' :
      case 'Q' :
           quit = 1;
           break;
    }
  }
  return 0;
}

Listing Nine
/***************** (Program output) ******************/
[ric1:macri]/u/macri/article/code1 >> atm
WELCOME TO JULIAN'S BANK
========================
Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? d
Deposit amount = 1000
A TRANSACTION FEE WAS CHARGED
Balance = $999.00
Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? i
Balance = $1063.94

Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? d
Deposit amount = 500
Balance = $1563.94

Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? w
Withdrawal amount = 1250
Balance = $313.93

Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? i
THIS ACCOUNT IS CURRENTLY NOT EARNING INTEREST
Balance = $313.93

Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? w
Withdrawal amount = 500
YOUR ACCOUNT IS OVERDRAWN
A TRANSACTION FEE WAS CHARGED
Balance = $-187.07

Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? w
Withdrawal amount = 200

YOU ARE NOT ALLOWED TO WITHDRAW FROM AN OVERDRAWN ACCOUNT
Balance = $-187.07

Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? d
Deposit amount = 1000
A TRANSACTION FEE WAS CHARGED
Balance = $811.94

Do you want to (d)eposit,(w)ithdraw, earn (i)nterest, or (q)uit? q
[ric1:macri]/u/macri/article/code1 >>

The Checker Implementation

In the checker implementation, the context class still contains all the main logic, while its state hierarchy contains very thin classes that respond to various checker methods. In this example, State and its derived classes (see Figure 2 and Listing Ten) all respond to isInterestBearing(), isNotInterestBearing(), isOverdrawn(), isNotOverdrawn(), requiresTransactionFee(), and doesNotRequireTransactionFee(). The positive methods (isInterestBearing(), isOverdrawn(), and requiresTransactionFee()) are declared as pure virtual functions in State. The negative methods (isNotInterestBearing(), isNotOverdrawn(), and doesNotRequireTransactionFee()), provided for readability, simply return the Boolean opposite of the positive methods.

Figure 2: The checker implementation.

Listing Ten
/****************** (State class definition) *********************/
class State
{
  public:
    static State * InitialState();
  public:
    virtual int isOverdrawn() = 0;
    int isNotOverdrawn() {return !this->isOverdrawn();};

    virtual int isInterestBearing() = 0;
    int isNotInterestBearing() {return !this->isInterestBearing();};

    virtual int requiresTransactionFee() = 0;
    int doesNotRequireTransactionFee() {return 
                                    !this->requiresTransactionFee();};
};
class InterestBearingState : public State
{
  public:
    virtual int isOverdrawn()            {return 0;};
    virtual int isInterestBearing()      {return 1;};
    virtual int requiresTransactionFee() {return 0;};
};
class NonInterestBearingState : public State
{
  public:
    virtual int isOverdrawn()            {return 0;};
    virtual int isInterestBearing()      {return 0;};
    virtual int requiresTransactionFee() {return 1;};
};
class OverdrawnState : public NonInterestBearingState
{
  public:
    virtual int isOverdrawn() {return 1;};
};

Once the state hierarchy and its checker methods have been implemented, the Account class can be developed. Listing Eleven contains the definition for the class. You will see that withdraw(amount) and payInterest() ask its current state checker questions to determine how to behave. For example, in payInterest(), if the state is interest bearing, then interest is paid; otherwise, the message is displayed to users indicating that it is a noninterest-bearing account. Similarly, the withdraw(amount) method checks its state to determine if the account is overdrawn.

Listing Eleven
/***************** (Account class definition) *****************/
#include "state.h"
#include "iostream.h"

class Account
{
  public:
    static double CurrentRate();
    static double CurrentMinimumBalance();
    static double CurrentTransactionFee();
  protected:
    enum EVENT
    {
      BALANCE_CHANGED,
      STATE_CHANGED
    };
  private:
    double  _balance;
    State * _state;
  protected:
    void event(EVENT event);
    State *   state() const {return _state;};
    void state(State * newState)
    {
      _state = newState;
      this->event(STATE_CHANGED);
    };
    void changeState(State * newState)
    {
      if (newState != this->state())
      {
        delete this->state();
        this->state(newState);
      }
    };
    void balance(double newBalance)
    {
      _balance = newBalance;
      this->event(BALANCE_CHANGED);
    };
    void sendNoticeToAccountHolder()
    {
         /* PRINT OUT AND MAIL A NOTICE INDICATING ACCOUNT OVERDRAWN */
      cout << "YOUR ACCOUNT IS OVERDRAWN" << endl;
    };
  public:
    double balance() { return _balance; };
    void deposit(double amount)
    {
      this->balance(this->balance() + amount);
    };
    void withdraw(double amount)
    {
      if (this->state()->isNotOverdrawn())
        this->balance(this->balance() - amount);
      else
        cout << "YOU ARE NOT ALLOWED TO WITHDRAW FROM 
                                        AN OVERDRAWN ACCOUNT" << endl;
    };
    void payInterest()
    {
      if (this->state()->isInterestBearing())
        this->balance(this->balance() * (1 + Account::CurrentRate()));
      else
        cout << "THIS ACCOUNT IS CURRENTLY NOT EARNING INTEREST" << endl;
    };
  public:
    Account()
      : _state(State::InitialState()),
        _balance(0)
    {};
    virtual ~Account()
    {
      delete this->state();
    };
};

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(context), 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].

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.