The State Pattern


Another technique for implementing FSMs is the STATE pattern.[1] This pattern combines much of the efficiency of the nested switch/case statement with much of the flexibility of interpreting a transition table.

[1] [GOF95], p. 305

Figure 36-2 shows the structure of the solution. The Turnstile class has public methods for the events and protected methods for the actions. It holds a reference to an interface called TurnstileState. The two derivatives of TurnstileState represent the two states of the FSM.

Figure 36-2. The STATE pattern for the Turnstile class


When one of the two event methods of Turnstile is invoked, it delegates that event to the TurnstileState object. The methods of TurnstileLockedState implement the appropriate actions for the Locked state. The methods of TurnstileUnlocked-State implement the appropriate actions for the Unlocked state. To change the state of the FSM, the reference in the Turnstile object is assigned to an instance of one of these derivatives.

Listing 36-7 shows the TurnstileState interface and its two derivatives. The state machine is easily visible in the four methods of those derivatives. For example, the Coin method of LockedTurnstileState tells the Turnstile object to change state to the unlocked state and then invokes the Unlock action function of Turnstile.

Listing 36-7. Turnstile.cs

public interface TurnstileState {   void Coin(Turnstile t);   void Pass(Turnstile t); } internal class LockedTurnstileState : TurnstileState {   public void Coin(Turnstile t)   {     t.SetUnlocked();     t.Unlock();   }   public void Pass(Turnstile t)   {     t.Alarm();   } } internal class UnlockedTurnstileState : TurnstileState {   public void Coin(Turnstile t)   {     t.Thankyou();   }   public void Pass(Turnstile t)   {     t.SetLocked();     t.Lock() ;   } }

The Turnstile class is shown in Listing 36-8. Note the static variables that hold the derivatives of TurnstileState. These classes have no variables and therefore never need to have more than one instance. Holding the instances of the TurnstileState derivatives in variables obviates the need to create a new instance every time the state changes. Making those variables static obviates the need to create new instances of the derivatives in the event that we need more than one instance of Turnstile.

Listing 36-8. Turnstile.cs

public class Turnstile {   internal static TurnstileState lockedState =     new LockedTurnstileState();   internal static TurnstileState unlockedState =     new UnlockedTurnstileState();   private TurnstileController turnstileController;   internal TurnstileState state = unlockedState;   public Turnstile(TurnstileController action)   {     turnstileController = action;   }   public void Coin()   {     state.Coin(this);   }   public void Pass()   {     state.Pass(this);   }   public void SetLocked()   {     state = lockedState;   }   public void SetUnlocked()   {     state = unlockedState;   }   public bool IsLocked()   {     return state == lockedState;   }   public bool IsUnlocked()   {     return state == unlockedState;   }   internal void Thankyou()   {     turnstileController.Thankyou();   }   internal void Alarm()   {     turnstileController.Alarm();   }   internal void Lock()   {     turnstileController.Lock();   }   internal void Unlock()   {     turnstileController.Unlock();   } }

State versus Strategy

Figure 36-2 is strongly reminiscent of the STRATEGY pattern.[2] Both have a context class, and both delegate to a polymorphic base class that has several derivatives. The difference (see Figure 36-3) is that in STATE, the derivatives hold a reference back to the context class. The primary function of the derivatives is to select and invoke methods of the context class through that reference. In the STRATEGY pattern, no such constraint or intent exists. The STRATEGY derivatives are not required to hold a reference to the context and are not required to call methods on the context. Thus, all instances of the STATE pattern are also instances of the STRATEGY pattern, but not all instances of STRATEGY are STATE.

[2] See Chapter 22.

Figure 36-3. STATE versus STRATEGY


Costs and Benefits

The STATE pattern provides a strong separation between the actions and the logic of the state machine. The actions are implemented in the Context class, and the logic is distributed through the derivatives of the State class. This makes it very simple to change one without affecting the other. For example, it would be very easy to reuse the actions of the Context class with a different state logic by simply using a different set of derivatives of the State class. Alternatively, we could create Context subclasses that modify or replace the actions without affecting the logic of the State derivatives.

Another benefit of this technique is that it is very efficient. It is probably just as efficient as the nested switch/case implementation. Thus, we have the flexibility of the table-driven approach with the efficiency of the nested switch/case approach.

The cost of this technique is twofold. First, the writing of the State derivatives is tedious at best. Writing a state machine with 20 states can be mind numbing. Second, the logic is distributed. There is no single place to go to see it all. This makes the code difficult to maintain. This is reminiscent of the obscurity of the nested switch/case approach.

The State Machine Compiler (SMC)

The tedium of writing the derivatives of State, and the need to have a single place to express the logic of the state machine led me to write the SMC compiler that I described in Chapter 15. The input to the compiler is shown in Listing 36-9. The syntax is

currentState {   event newState action   ... }


The four lines at the top of Listing 36-9 identify the name of the state machine, the name of the context class, the initial state, and the name of the exception that will be thrown in the event of an illegal event.

Listing 36-9. Turnstile.sm

FSMName Turnstile Context TurnstileActions Initial Locked Exception FSMError {     Locked     {         Coin    Unlocked    Unlock         Pass    Locked      Alarm     }     Unlocked     {         Coin    Unlocked    Thankyou         Pass    Locked      Lock     } }

In order to use this compiler, you must write a class that declares the action functions. The name of this class is specified in the Context line. I called it TurnstileActions. See Listing 36-10.

Listing 36-10. TurnstileActions.cs

public abstract class TurnstileActions {   public virtual void Lock() {}   public virtual void Unlock() {}   public virtual void Thankyou() {}   public virtual void Alarm() {} }

The compiler generates a class that derives from the context. The name of the generated class is specified in the FSMName line. I called it Turnstile.

I could have implemented the action functions in TurnstileActions. However, I am more inclined to write another class that derives from the generated class and implements the action functions there. This is shown in Listing 36-11.

Listing 36-11. TurnstileFSM.cs

public class TurnstileFSM : Turnstile {   private readonly TurnstileController controller;   public TurnstileFSM(TurnstileController controller)   {     this.controller = controller;   }   public override void Lock()   {     controller.Lock();   }   public override void Unlock()   {     controller.Unlock();   }   public override void Thankyou()   {     controller.Thankyou();   }   public override void Alarm()   {     controller.Alarm();   } }

That's all we have to write. SMC generates the rest. The resulting structure is shown in Figure 36-4. We call this a three-level FSM.[3]

[3] [PLOPD1], p. 383

Figure 36-4. Three-level FSM


The three levels provide the maximum in flexibility at a very low cost. We can create many different FSMs simply by deriving them from TurnstileActions. We can also implement the actions in many different ways simply by deriving from Turnstile.

Note that the generated code is completely isolated from the code that you have to write. You never have to modify the generated code. You don't even have to look at it. You can pay it the same level of attention that you pay to binary code.

Turnstile.cs Generated by SMC, and Other Support Files

Listings 36-12 through 36-14 complete the code for the SMC example of the turnstile. Turnstile.cs was generated by SMC. The generator creates a bit of cruft, but the code is not bad.

Listing 36-12. Turnstile.cs

//---------------------------------------------- // // FSM:       Turnstile // Context:   TurnstileActions // Exception: FSMError // Version: // Generated: Monday 07/18/2005 at 20:57:53 CDT // //---------------------------------------------- //---------------------------------------------- // // class Turnstile //    This is the Finite State Machine class // public class Turnstile : TurnstileActions {   private State itsState;   private static string itsVersion = " ";   // instance variables for each state   private Unlocked itsUnlockedState;   private Locked itsLockedState;   // constructor   public Turnstile()   {     itsUnlockedState = new Unlocked();     itsLockedState = new Locked();     itsState = itsLockedState;     // Entry functions for: Locked   }   // accessor functions   public string GetVersion()   {     return itsVersion;   }   public string GetCurrentStateName()   {     return itsState.StateName();   }   public State GetCurrentState()   {     return itsState;   }   public State GetItsUnlockedState()   {     return itsUnlockedState;   }   public State GetItsLockedState()   {     return itsLockedState;   }   // Mutator functions   public void SetState(State value)   {     itsState = value;   }   // event functions - forward to the current State   public void Pass()   {     itsState.Pass(this);   }   public void Coin()   {     itsState.Coin(this);   } } //-------------------------------------------- // // public class State //    This is the base State class // public abstract class State {   public abstract string StateName();   // default event functions   public virtual void Pass(Turnstile name)   {        throw new FSMError( "Pass", name.GetCurrentState());   }   public virtual void Coin(Turnstile name)   {       throw new FSMError( "Coin", name.GetCurrentState());   } } //-------------------------------------------- // // class Unlocked //    handles the Unlocked State and its events // public class Unlocked : State {   public override string StateName()     { return "Unlocked"; }   //   // responds to Coin event   //   public override void Coin(Turnstile name)   {     name.Thankyou();     // change the state     name.SetState(name.GetItsUnlockedState());   }   //   // responds to Pass event   //   public override void Pass(Turnstile name)   {     name.Lock();      // change the state      name.SetState(name.GetItsLockedState());   } } //-------------------------------------------- // // class Locked //    handles the Locked State and its events // public class Locked : State {   public override string StateName()     { return "Locked"; }   //   // responds to Coin event   //   public override void Coin(Turnstile name)   {     name.Unlock();     // change the state     name.SetState(name.GetItsUnlockedState());   }   //   // responds to Pass event   //   public override void Pass(Turnstile name)   {     name.Alarm();     // change the state     name.SetState(name.GetItsLockedState());   } }

FSMError is the exception that we told SMC to throw in case of an illegal event. The turnstile example is so simple that there can't be an illegal event, so the exception is useless. However, larger state machines have events that should not occur in certain states. Those transitions are never mentioned in the input to SMC. Thus, if such an event were ever to occur, the generated code would throw the exception.

Listing 36-13. FSMError.cs

public class FSMError : ApplicationException {   private static string message =     "Undefined transition from state: {0} with event: {1}.";   public FSMError(string theEvent, State state)     : base(string.Format(message, state.StateName(), theEvent))   {   } }

The test code for the SMC-generated state machine is very similar to all the other test programs we've written in this chapter. The differences are minor.

Listing 36-14.

[TestFixture] public class SMCTurnstileTest {   private Turnstile turnstile;   private TurnstileControllerSpoof controllerSpoof;   private class TurnstileControllerSpoof : TurnstileController   {     public bool lockCalled = false;     public bool unlockCalled = false;     public bool thankyouCalled = false;     public bool alarmCalled = false;     public void Lock(){lockCalled = true;}     public void Unlock(){unlockCalled = true;}     public void Thankyou(){thankyouCalled = true;}     public void Alarm(){alarmCalled = true;}   }   [SetUp]   public void SetUp()   {     controllerSpoof = new TurnstileControllerSpoof();     turnstile = new TurnstileFSM(controllerSpoof);   }   [Test]   public void InitialConditions()   {     Assert.IsTrue(turnstile.GetCurrentState() is Locked);   }   [Test]   public void CoinInLockedState()   {     turnstile.SetState(new Locked());     turnstile.Coin();     Assert.IsTrue(turnstile.GetCurrentState() is Unlocked);     Assert.IsTrue(controllerSpoof.unlockCalled);   }   [Test]   public void CoinInUnlockedState()   {     turnstile.SetState(new Unlocked());     turnstile.Coin();     Assert.IsTrue(turnstile.GetCurrentState() is Unlocked);     Assert.IsTrue(controllerSpoof.thankyouCalled);   }   [Test]   public void PassInLockedState()   {     turnstile.SetState(new Locked());     turnstile.Pass();     Assert.IsTrue(turnstile.GetCurrentState() is Locked);     Assert.IsTrue(controllerSpoof.alarmCalled);   }   [Test]   public void PassInUnlockedState()   {     turnstile.SetState(new Unlocked());     turnstile.Pass();     Assert.IsTrue(turnstile.GetCurrentState() is Locked);     Assert.IsTrue(controllerSpoof.lockCalled);   } }

The TurnstileController class is identical to all the others that appeared in this chapter. You can see it in Listing 36-2.

Following is the DOS command used to invoke SMC. You'll note that SMC is a Java program. Although it's written in Java, it is capable of generating C# code in addition to Java and C++ code.

java -classpath .\smc.jar smc.Smc -g smc.generator.csharp.SMCSharpGenerator turnstileFSM.sm


Costs and Benefits

Clearly, we've managed to maximize the benefits of the various approaches. The description of the FSM is contained in once place and is very easy to maintain. The logic of the FSM is strongly isolated from the implementation of the actions, enabling each to be changed without impact on the other. The solution is efficient and elegant and requires a minimum of coding.

The cost is in the use of SMC. You have to procure, and learn how to use, another tool. In this case, however, the tool is remarkably simple to install and use, and it's free.




Agile Principles, Patterns, and Practices in C#
Agile Principles, Patterns, and Practices in C#
ISBN: 0131857258
EAN: 2147483647
Year: 2006
Pages: 272

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net