Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

State Patterns & C++


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


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.