TheStatePattern

From Pattern Repository Wiki
Jump to: navigation, search

The State Pattern

Webinar Link

Contextual Forces

Motivation

The motivation of the State Pattern is to increase the flexibility and maintainability of a modal system by defining each mode (state) as its own type, and then to encapsulate the set of modes behind an interface. This increases encapsulation and provides a unifying interface through which the modal system ("state machine") must interact with its current state.

Encapsulation

The State Pattern encapsulates the rules and attributes of individual states. The State Pattern *should* encapsulate the following:

  • The number of possible states (to some extent)
  • State-specific data
  • State-specific behaviors

In addition, individual states *may also* encapsulate the rules governing a shifts to another state but it does not have to.

Procedural Analog

One of the classic procedural analogs is to track a state machine's state in an integer and use a switch statement based on that number to determine behavior.

typedef enum
{
  Closed,
  Opened,
  Fault
} ConnectionState;

typedef struct
{
  ConnectionState state;
  void* privateData;
} Connection;

#define INVALID_STATE -1

int OpenConnection(Connection* toOpen)
{
  switch (toOpen->State)
  {
    case Opened:
    case Fault:
      return INVALID_STATE;
    case Closed:
      open_connection(toOpen->void*);
    break;
    default:
      return INVALID_STATE;
  }

  return 0;
}


Non-Software Analog

When you go to a movie you are issued a ticket. This ticket - or the absence thereof - encapsulates your privileges to see the movie for which you paid. It grants to entry into the theater and in so doing becomes torn. Once you have a torn ticket you are able to enter and leave the theater freely for the duration of the movie. Any time you are inside the theater, you may be subjected to a test: an usher may check your ticket to make sure you are properly authorized for the movie you are viewing.


Implementation Forces

Example

Movie Ticket Example.jpg

When an instance of Moviegoer tries to enter a theater, an Usher takes its Ticket by calling TakeTicket(). The instance of Usher then calls CanEnterTheater() to determine whether or not the Moviegoer object is permitted to enter. If the Moviegoer is allowed to enter the theater, the Usher calls Redeem() and gives the ticket stub back to the Moviegoer via the GiveTicket() method.

When an instance of Usher wants to check a Moviegoer's ticket inside the theater, it calls Moviegoer's TakeTicket() method. Moviegoer will then present an instance of either FullTicket, TicketStub, or NoTicket. Usher is then able to call CanBeInTheater() to determine whether the Moviegoer should be there or not. If the instance of Moviegoer does not have the right to be in the theater, it is ejected. Regardless the Moviegoer object's ticket is returned to it via the GieTicket() method.

public interface Ticket
{
  bool CanEnterMovie();
  bool CanBeInMovie();

  Ticket Redeem();
}

internal class NoTicket : Ticket
{
  #region Ticket Members

  public bool CanEnterMovie()
  {
    return false;
  }

  public bool CanBeInMovie()
  {
    return false;
  }

  public Ticket Redeem()
  {
    throw new InvalidOperationException("You cannot redeem nothing");
  }

  #endregion
}

internal class TicketStub : Ticket
{
  #region Ticket Members

  public bool CanEnterMovie()
  {
    return true;
  }

  public bool CanBeInMovie()
  {
    return true;
  }

  public Ticket Redeem()
  {
    return this;
  }

  #endregion
}

internal class FullTicket : Ticket
{
  private readonly TicketStub _Stub = new TicketStub();

  #region Ticket Members

  public bool CanEnterMovie()
  {
    return true;
  }

  public bool CanBeInMovie()
  {
    return false;
  }

  public Ticket Redeem()
  {
    return _Stub;
  }

  #endregion
}

internal class Moviegoer
{
  private Ticket _Ticket;

  internal void GiveTicket(Ticket ticket)
  {
    _Ticket = ticket;
  }

  internal Ticket TakeTicket()
  {
    Ticket result = _Ticket;
    _Ticket = null;
    return result ?? new NoTicket();
  }
}

Questions, concerns, credibility checks

  1. Sometimes people have trouble distinguishing between The State and Strategy Patterns. The line is, indeed, fuzzy and sometimes the same code ends up being an implementation of both. The principle difference is, of course, intent: the purpose of the State Pattern is to model a state machine whereas the purpose of Strategy is to encapsulate algorithms in such a way as to make them interchangeable.
  2. State machines are not required nearly as often as they are used so the use of this pattern should be a yellow flag. Before you use it step back and ask yourself the question "Do I even need to do this? Is there a cleaner approach?" In general, you will find them on the surface of your system and at its core. Some good places to implement a state machine might be:
    1. Business entites; they live a long time and change during the course of their lifespan (e.g.: an account can be closed)
    2. Interfaces with external actors such as a user or a database

Options in implementation

This is such a flexible pattern that it is difficult to codify or categorize the volume of implementation options we have. We will only address one such question in this topic: When do we encapsulate state transitions? The trivial answer - which also happens to be the only correct answer - is "When it makes sense" but there are some guidelines you can use to help make that determination.

  1. How often are state shifts initiated by stimulus applied to the machine?
  2. How often are state shifts initiated by stimulus applied to a specific state?
  3. How uniform from state, to state, are the sets of events that can trigger a state shift?

It's not really an "either/or" type question. As your state machine grows in complexity it is increasingly likely that you will need to have the machine and its states collaborate in transitions. It's more of a question of "where do you start out?" The true answer is that if you are using good practices, it really isn't that important. Eventually, you will develop a "feel" for when to have the state machine manage its transitions versus having its individual states do so. You may already have that intuition but, if you don't, just make sure you use good practices with solid encapsulation and you will converge on the right answer.

Consequent Forces

Testing issues

Testing a state machine can put you to the test. If you haven't already exposed your state interface to permit extension, the need to mock or fake out the states and the need to encapsulate the them behind the machine can be in direct competition. For simple machines, it seems like it is best to treat the machine and its idividual states as a single unit thereby leaving the state classes encapsulated. For more complex systems you have two options both have costs and benefits:

  1. Write your tests in such a way as to build off of each other
  2. Inject a dependency inversion

The first option is a pretty strong one if you have an easy way of distinguishing between a test that fails and a test that was inconclusive. If you don't have that, you probably should inject a dependency inversion (e.g. expose your state interface).

Cost-Benefit (gain-loss)

The benefit of the State Pattern is clear: it divides the responsabilities up nicely between a state machine and its individual states. Thus cohesion and focus are both improved. There can be an imperceptible performance hit due to the use of virtual methods but that is hardly worth mentioning. There are a few maintenance costs associated with it, in that if your state interface mutates, you must propagate that mutation throughout all of the individual state classes. However, that pales in comparison to the cost of managing your state model when you are using switch statements, so that's not very relevant either. It seems that the biggest cost of the State Pattern is that it can be misapplied and used to justify mutability that exists in the stead of better practices. Generally, mutability is bad. There are exceptions but on balance, an object should be born ready to do its job. There are few good cases for mutability that the Builder Pattern cannot cover. Fortunately, this is a self-mitigating problem. The cost of implementating the State Pattern to encapsulate "bad" mutability is prohibitively high.