Contents
State
From GoF: Allow an object to alter its behaviour when its internal state
changes. The object will appear to change its class.
Use State when an object's behaviour is a function of its state and it must
hange its behaviour at run-time depending on that state. Examples of such
objects are usually parsers of some sort: objects which translate one structure
into another such as an object which takes an expression like 'A & (B v C
& (!D v A)) & E' and restructures as the BooleanExpression
Interpreter pattern we looked at.
State is an implementation of a Finite State Machine.
A Formal Definition of a State Machine
The State Pattern is a Finite State Machine (FSM), a concept
from Computer Science. A definition of an FSM from America's
NIST:
A model of computation consisting of
a set of states, a start state, an input alphabet, and a transition function that maps input
symbols and current states to a next state. Computation begins in the start state with
an input string. It changes to new states depending on the transition function.
There are many variants, for instance, machines having actions (outputs)
associated with transitions (Mealy machine) or states (Moore machine), multiple start
states, transitions conditioned on no input symbol (a null) or more than one
transition for a given symbol and state (nondeterministic finite state machine),
one or more states designated as accepting states (
recognizer), etc
Basic Pattern
The basic pattern follows the following sequence:
-
Context sets up required data, and reads the source structure one element at a
time.
- For each element, it calls the current state object passing the
element read.
- The State object processes the element and translates it
as required, passing the result back to a property or method of Context.
Depending on the results of its processing, it creates an instance of the
appropriate State object to process the next element and passes it back as a
return.
- The Context receives the new state object and uses it to
process the next element.
- At the end, the Context returns the new
structure built by the state objects.
There are two important bits here:
-
How is the next State object decided?
- How do the State objects store their
output in the Context?
Example using BooleanInterpreter
The first depends on the grammar defined for the input and output structures.
The second is a matter of convenience � any way that makes sense. The first
will generally be helped by a State Diagram. This is one for the
BooleanExpression parser:
This leads us to 4 possible States:
-
StartState: expecting the start of a binary operation. Valid characters are a
Variable name, "(", and "!". The former transitions to OperatorState where an
Operator is expected. "!" is pushed onto the Stack and the State is unchanged.
"(" is discarded and the state is unchanged.
-
OperatorState: Only valid operators are expected are "&" and "v". The
operation is pushed onto the stack and the state changed to SecondState.
-
SecondState: This expects the second Boolean in a binary expression. Valid
characters are a Variable Name, "!", and "(". A Variable name it put on the
stack and the stack collapsed into BooleanExpressions which are replaced on the
stack; the State changes to EndState. A "!" is placed on the stack and the
state remains unchanged. A "(" is discarded but the state changes to
StartState.
-
EndState: This covers various ways a Boolean binary can terminate. Valid
characters are ")", and an operation. A ")" is discarded and the state changed
to OperatorState. An Operation is put on the stack and the state changed to
SecondState.
The CollapseStack method could have been in the Context object, but it was
originally coded in SecondState as the only state which used it, and I haven't
changed it. The code for the Context object (BooleanParser) and the states
follows. Note the abstract BooleanState from which the concrete states inherit.
public class BooleanParser {
protected string expression;
public BooleanParser(string expression) {
this.expression = expression.Replace(" ", "");
}
protected Stack stack;
public BooleanExpression Parse() {
stack = new Stack();
BooleanState currentState = new StartState(this);
for (int i = 0; i < this.expression.Length; i++) {
currentState =
currentState.Handle(this.expression[i].ToString());
}
return stack.Pop() as BooleanExpression;
}
internal Stack Stack {
get { return this.stack; }
}
}
public abstract class BooleanState {
public BooleanState(BooleanParser parser) {
this.parser = parser;
}
protected BooleanParser parser;
public virtual BooleanState Handle(string token) {
switch (token) {
case "&":
case "v":
return ProcessOp(token);
case "!":
return ProcessNot();
case "(":
return ProcessOpen();
case ")":
return ProcessClose();
default:
return ProcessVariable(token);
}
}
public virtual BooleanState ProcessOp(string token) {
throw new ApplicationException("Invalid expression");
}
public virtual BooleanState ProcessNot() {
throw new ApplicationException("Invalid expression");
}
public virtual BooleanState ProcessOpen() {
throw new ApplicationException("Invalid expression");
}
public virtual BooleanState ProcessClose() {
throw new ApplicationException("Invalid expression");
}
public virtual BooleanState ProcessVariable(string token) {
throw new ApplicationException("Invalid expression");
}
}
public class StartState : BooleanState {
public StartState(BooleanParser parser) : base(parser) {
}
public override BooleanState ProcessOp(string token) {
throw new ApplicationException("Invalid expression");
}
public override BooleanState ProcessNot() {
parser.Stack.Push("!");
return this;
}
public override BooleanState ProcessOpen() {
return this;
}
public override BooleanState ProcessClose() {
throw new ApplicationException("Invalid expression");
}
public override BooleanState ProcessVariable(string token) {
parser.Stack.Push(new VariableBooleanExpression(token));
return new OperatorState(parser);
}
}
public class OperatorState : BooleanState {
public OperatorState(BooleanParser parser) : base(parser) {
}
public override BooleanState ProcessOp(string token) {
parser.Stack.Push(token);
return new SecondState(parser);
}
}
public class SecondState : BooleanState {
public SecondState(BooleanParser parser) : base(parser) {
}
public override BooleanState ProcessNot() {
parser.Stack.Push("!");
return this;
}
public override BooleanState ProcessOpen() {
return new StartState(parser);
}
public override BooleanState ProcessVariable(string token) {
parser.Stack.Push(token);
CollapseStack();
return new EndState(parser);
}
private void CollapseStack() {
object second = parser.Stack.Pop();
BooleanExpression secondExp = second as BooleanExpression;
if (secondExp == null) {
secondExp = new VariableBooleanExpresion(second as string);
}
string opA = parser.Stack.Pop() as string;
if (opA == "!") {
BooleanExpression innerExp = secondExp;
secondExp = new NotBooleanExpression(innerExp);
opA = parser.Stack.Pop() as string;
}
object first = parser.Stack.Pop();
BooleanExpression firstExp = first as BooleanExpression;
if (firstExp == null) {
firstExp = new VariableBooleanExpression(first as string);
}
if ((parser.Stack.Count > 0)&&(parser.Stack.Peek() as string) == "!") {
parser.Stack.Pop();
BooleanExpression innerExp = firstExp;
firstExp = new NotBooleanExpression(innerExp);
}
BooleanExpression finalExp = null;
if (opA == "&") {
finalExp = new AndBooleanExpression(firstExp, secondExp);
} else {
finalExp = new OrBooleanExpression(firstExp, secondExp);
}
parser.Stack.Push(finalExp);
if (parser.Stack.Count > 1) {
CollapseStack();
}
}
}
public class EndState : BooleanState {
public EndState(BooleanParser parser) : base(parser) {
}
public override BooleanState ProcessOp(string token) {
parser.Stack.Push(token);
return new SecondState(parser);
}
public override BooleanState ProcessClose() {
return new OperatorState(parser);
}
}
Final comments
This is a medium-complex instance of State: there are not many different States,
but the processing (using a Stack and the CollapseStack method) is not entirely
simple. However, I think that the code is reasonably clear. A much simpler
example of State is given by Robert C Martin Robert C Martin)
at Yonat Sharon's excellent OOTips site:
There is a specific design pattern that solves this problem.
It is called "State"; and can be found in the GOF book
|Account|<#>----|AccountState|
^ A
| |
| +-----+-----+
| | |
| |OpenState| |ClosedState|
| | |
+-----------+---------------+
Account contains a base class named AccountState.
There are two subclasses of AccountState named OpenState
and ClosedState. Each of these contain a reference back
to the |Account|.
This allows methods of Account that are affected by the
state to be delegated to the contained state object.
The state object then calls the appropriate functions
in the Account object.
That's a basic application of State. The advantages of State include:
- State behaviour is localised,
simplifying changing or adding states
- State objects eliminate the need for
complex if/switch structures, making the code easier to read and far more
maintainable.
- State transitions are made explicit, as is state-dependant
processing
Other notes:
- The States could be implemented as nested classes
belonging to the Context class. They have no use outside that class, in this
example, and could thus be declared within it. This is not uncommon.
- State
classes do not contain state themselves and so can be Singletons or Flyweights
- The contents and structure of Context follow directly from a) its clients and
b) what its State objects require from it. Usually the clients will require
nothing more than a method similar to the Parse() method in the example. The
requirements of the State objects depend on what is being achieved: in this
case they needed a Stack to store temporary working data and which ended up
containing the final structure.
- Context can make the decision on the
successor state, rather than the state objects themselves. This would probably
increase the communication needs between each State instance and the Context.
Some people think that Context is the better or 'natural' place for that
decision to be made. In all the examples I've done the natural place was in the
State objects, but I may be missing something; it's probably a matter of style.
- If something only has two or three states, it is not overkill to use the
State pattern.