36. State

image

© Jennifer M. Kohnke

A state without the means of some change is without the means of its conservation.

—Edmund Burke (1729–1797)

Finite state automata are among the most useful abstractions in the software arsenal and are almost universally applicable. They provide a simple and elegant way to explore and define the behavior of a complex system. They also provide a powerful implementation strategy that is easy to understand and easy to modify. I use them in all levels of a system, from controlling the high-level GUI to the lowest-level communication protocols.

We studied the notation and basic operation of FSMs in Chapter 15. Now let’s look at the patterns for implementing them. Consider the subway turnstile in Figure 36-1 once again.

Figure 36-1. Turnstile FSM that covers abnormal events

image

Nested Switch/Case Statements

There are many different strategies for implementing an FSM. The first, and most direct, is through nested switch/case statements. Listing 36-1 shows one such implementation.


Listing 36-1. Turnstile.cs (nested switch/case implementation)

public enum State {LOCKED, UNLOCKED};
public enum Event {COIN, PASS};

public class Turnstile
{
  // Private
  internal State state = State.LOCKED;

  private TurnstileController turnstileController;

  public Turnstile(TurnstileController action)
  {
    turnstileController = action;
  }

  public void HandleEvent(Event e)
  {
    switch (state)
    {
      case State.LOCKED:
        switch (e)
        {
          case Event.COIN:
            state = State.UNLOCKED;
            turnstileController.Unlock();
            break;
          case Event.PASS:
            turnstileController.Alarm();
            break;
        }
        break;
      case State.UNLOCKED:

        switch (e)
        {
          case Event.COIN:
            turnstileController.Thankyou();
            break;
          case Event.PASS:
            state = State.LOCKED;
            turnstileController.Lock();
            break;
        }
        break;
    }
  }
}


The nested switch/case statement divides the code into four mutually exclusive zones, each corresponding to one of the transitions in the STD. Each zone changes the state as needed and then invokes the appropriate action. Thus, the zone for Locked and Coin changes the state to Unlocked and calls Unlock.

Some interesting aspects to this code have nothing to do with the nested switch/case statement. In order for them to make sense, you need to see the unit test that I used to check this code. See Listings 36-2 and 36-3.


Listing 36-2. TurnstileController.cs

public interface TurnstileController
{
  void Lock();
  void Unlock();
  void Thankyou();
  void Alarm();
}



Listing 36-3. TurnstileTest.cs

[TestFixture]
public class TurnstileTest
{
  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 Turnstile(controllerSpoof);
  }

  [Test]
  public void InitialConditions()
  {
    Assert.AreEqual(State.LOCKED, turnstile.state);
  }

  [Test]
  public void CoinInLockedState()
  {
    turnstile.state = State.LOCKED;
    turnstile.HandleEvent(Event.COIN);
    Assert.AreEqual(State.UNLOCKED, turnstile.state);
    Assert.IsTrue(controllerSpoof.unlockCalled);
  }

  [Test]
  public void CoinInUnlockedState()
  {
    turnstile.state = State.UNLOCKED;
    turnstile.HandleEvent(Event.COIN);
    Assert.AreEqual(State.UNLOCKED, turnstile.state);
    Assert.IsTrue(controllerSpoof.thankyouCalled);
  }

  [Test]
  public void PassInLockedState()
  {
    turnstile.state = State.LOCKED;
    turnstile.HandleEvent(Event.PASS);
    Assert.AreEqual(State.LOCKED, turnstile.state);
    Assert.IsTrue(controllerSpoof.alarmCalled);
  }

  [Test]
  public void PassInUnlockedState()
  {
    turnstile.state = State.UNLOCKED;
    turnstile.HandleEvent(Event.PASS);
    Assert.AreEqual(State.LOCKED, turnstile.state);
    Assert.IsTrue(controllerSpoof.lockCalled);
  }
}


The Internal Scope State Variable

Note the four test functions CoinInLockedState, CoinInUnlockedState, PassInLockedState, and PassInUnlockedState. These functions test the four transitions of the FSM separately by forcing the state variable of the Turnstile to the state they want to check and then invoking the event they want to verify. In order for the test to access the state variable c, it cannot be private. So I gave it internal access and wrote a comment indicating my intent to make it private.

Object-oriented dogma insists that all instance variables of a class ought to be private. I have blatantly ignored this rule, and by doing so, I have broken the encapsulation of Turnstile.

Or have I? Make no mistake about it: I would rather have kept the state variable private. However, to do so would have denied my test code the ability to force its value. I could have created the appropriate CurrentState get and set property with internal scope, but that seems ridiculous. I was not trying to expose the state variable to any class other than TestTurnstile, so why should I create a get and set property that implies that anyone in the assembly can get and set that variable?

Testing the Actions

Note the TurnstileController interface in Listing 36-2. This was put in place specifically so that the TestTurnstile class could ensure that the Turnstile class was invoking the right action methods in the right order. Without this interface, it would have been much more difficult to ensure that the state machine was working properly.

This is an example of the impact that testing has on design. Had I simply written the state machine without giving thought to testing, it is unlikely that I would have created the TurnstileController interface. That would have been unfortunate. The Turnstile-Controller interface nicely decouples the logic of the FSM from the actions it needs to perform. Another FSM, using very different logic, can use the TurnstileController without any impact at all.

The need to create test code that verifies each unit in isolation forces us to decouple the code in ways we might not otherwise think of. Thus, testability is a force that drives the design to a less coupled state.

Costs and Benefits

For simple state machines, the nested switch/case implementation is both elegant and efficient. All the states and events are visible on one or two pages of code. However, for larger FSMs, the situation changes. In a state machine with dozens of states and events, the code devolves into page after page of case statements. There are no convenient locators to help you see where, in the state machine, you are reading. Maintaining long, nested switch/case statements can be a very difficult and error-prone job.

Another cost of the nested switch/case is that there is no good separation between the logic of the FSM and the code that implements the actions. That separation is strongly present in Listing 36-1 because the actions are implemented in a derivative of the TurnstileController. However, in most nested switch/case FSMs that I have seen, the implementation of the actions is buried in the case statements. Indeed, this is still possible in Listing 36-1.

Transition Tables

A common technique for implementing FSMs is to create a data table that describes the transitions. This table is interpreted by an engine that handles the events. The engine looks up the transition that matches the event, invokes the appropriate action, and changes the state. Listing 36-4 shows the code that creates the transition table, and Listing 36-5 shows the transition engine. Both of these listings are snippets from the full implementation (Listing 36-6).


Listing 36-4. Building the turnstile transition table

public Turnstile(TurnstileController controller)
{
  Action unlock = new Action(controller.Unlock);
  Action alarm = new Action(controller.Alarm);
  Action thankYou = new Action(controller.Thankyou);
  Action lockAction = new Action(controller.Lock);

  AddTransition(
    State.LOCKED,   Event.COIN, State.UNLOCKED, unlock);
  AddTransition(
    State.LOCKED,   Event.PASS, State.LOCKED,   alarm);
  AddTransition(
    State.UNLOCKED,   Event.COIN, State.UNLOCKED, thankYou);
  AddTransition(
    State.UNLOCKED,   Event.PASS, State.LOCKED,   lockAction);
}



Listing 36-5. The transition engine

public void HandleEvent(Event e)
{
  foreach(Transition transition in transitions)
  {
    if(state == transition.startState &&
      e == transition.trigger)
    {
      state = transition.endState;
      transition.action();
    }
  }
}


Using Table Interpretation

Listing 36-6 is the full implementation showing how a finite state machine can be implemented by interpreting a list of transition data structures. This code is completely compatible with TurnstileController (Listing 36-2) and the TurnstileTest (Listing 36-3).


Listing 36-6. Turnstile.cs full implementation

Turnstile.cs using table interpretation.
public enum State {LOCKED, UNLOCKED};
public enum Event {COIN, PASS};

public class Turnstile
{
  // Private
  internal State state = State.LOCKED;

  private IList transitions = new ArrayList();

  private delegate void Action();

  public Turnstile(TurnstileController controller)
  {
    Action unlock = new Action(controller.Unlock);
    Action alarm = new Action(controller.Alarm);
    Action thankYou = new Action(controller.Thankyou);
    Action lockAction = new Action(controller.Lock);

    AddTransition(
      State.LOCKED,   Event.COIN, State.UNLOCKED, unlock);
    AddTransition(
      State.LOCKED,   Event.PASS, State.LOCKED,   alarm);
    AddTransition(
      State.UNLOCKED, Event.COIN, State.UNLOCKED, thankYou);
    AddTransition(
      State.UNLOCKED, Event.PASS, State.LOCKED,   lockAction);
  }

  public void HandleEvent(Event e)
  {
    foreach(Transition transition in transitions)
    {
      if(state == transition.startState &&
        e == transition.trigger)
      {
        state = transition.endState;
        transition.action();
      }
    }
  }

  private void AddTransition(State start, Event e, State end, Action
action)
  {
    transitions.Add(new Transition(start, e, end, action));
  }

  private class Transition
  {
    public State startState;
    public Event trigger;
    public State endState;
    public Action action;

    public Transition(State start, Event e, State end,Action a)
    {
      this.startState = start;
      this.trigger = e;
      this.endState = end;
      this.action = a;
    }
  }
}


Costs and Benefits

One powerful benefit is that the code that builds the transition table reads like a canonical state transition table. The four AddTransition lines can be very easily understood. The logic of the state machine is all in one place and is not contaminated with the implementation of the actions.

Maintaining an FSM like this is very easy compared to the nested switch/case implementation. To add a new transition, one simply adds a new AddTransition line to the Turnstile constructor.

Another benefit of this approach is that the table can easily be changed at runtime. This allows for dynamic alteration of the logic of the state machine. I have used mechanisms like that to allow hot patching of FSMs.

Still another benefit is that multiple tables can be created, each representing a different FSM logic. These tables can be selected at runtime, based on starting conditions.

The cost of the approach is primarily speed. It takes time to search through the transition table. For large state machines, that time may become significant.

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.

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

image

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.

Figure 36-3. STATE versus STRATEGY

image

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

Figure 36-4. Three-level FSM

image

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.

Classes of State Machine Application

I use state machines and SMC for several classes of application.

High-Level Application Policies for GUIs

One of the goals of the graphical revolution in the 1980s, was to create stateless interfaces for humans to use. At that time, computer interfaces were dominated by textual approaches using hierarchical menus. It was easy to get lost in the menu structure, losing track of what state the screen was in. GUIs helped mitigate that problem by minimizing the number of state changes that the screen went through. In modern GUIs, a great deal of work is put into keeping common features on the screen at all times and making sure that the user does not get confused by hidden states.

It is ironic, then, that the code that implements these “stateless” GUIs is strongly state driven. In such GUIs, the code must figure out which menu items and buttons to gray out, which subwindows should appear, which tab should be activated, where the focus ought to be put, and so on. All these decisions are decisions about the state of the interface.

I learned a long time ago that controlling these factors is a nightmare unless you organize them into a single control structure. That control structure is best characterized as an FSM. Since those days, I have been writing almost all my GUIs using FSMs generated by SMC or its predecessors.

Consider the state machine in Listing 36-15. This machine controls the GUI for the login portion of an application. On getting a start event, the machine puts up a login screen. Once the user presses the Enter key, the machine checks the password. If the password is good, the machine goes to the loggedIn state and starts the user process (not shown). If the password is bad, the machine displays a screen so informing the user. The user can try again by clicking the OK button but otherwise clicks the Cancel button. If a bad password is entered three times in a row (thirdBadPassword event), the machine locks the screen until the administrator password is entered.


Listing 36-15. login.sm

Initial init
{
  init
  {
    start logginIn displayLoginScreen
  }

  logginIn
  {
    enter checkingPassword checkPassword
    cancel init clearScreen
  }

  checkingPassword
  {
    passwordGood loggedIn startUserProcess
    passwordBad notifyingPasswordBad displayBadPasswordScreen
    thirdBadPassword screenLocked displayLockScreen
  }
  notifyingPasswordBad
  {
    OK checkingPassword displayLoginScreen
    cancel init clearScreen
  }

  screenLocked
  {
    enter checkingAdminPassword checkAdminPassword
  }

  checkingAdminPassword
  {
    passwordGood init clearScreen
    passwordBad screenLocked displayLockScreen
  }
}


What we’ve done here is to capture the high-level policy of the application in a state machine. This high-level policy lives in one place and is easy to maintain. It vastly simplifies the rest of the code in the system, because that code is not mixed with the policy code.

Clearly, this approach can be used for interfaces other than GUIs. Indeed, I have used similar approaches for textual and machine/machine interfaces as well. But GUIs tend to be more complex than those others, so the need for them, and the volume of them, is greater.

GUI Interaction Controllers

Imagine that you want to allow your users to draw rectangles on the screen. The gestures they use are as follows. A user clicks the rectangle icon in the pallet window, positions the mouse in the canvas window at one corner of the rectangle, presses the mouse button, and drags the mouse toward the desired second corner. As the user drags, an animated image of the potential rectangle appears on the screen. The user manipulates the rectangle to the desired shape by continuing to hold the mouse button down while dragging the mouse. When the rectangle is right, the user releases the mouse button. The program then stops the animation and draws a fixed rectangle on the screen.

Of course, the user can abort this at any time by clicking a different pallet icon. If the user drags the mouse out of the canvas window, the animation disappears. If the mouse returns to the canvas window, the animation reappears.

Finally, having finished drawing a rectangle, the user can draw another one simply by clicking and dragging again in the canvas window. There is no need to click the rectangle icon in the pallet.

What I have described here is a FSM. The state transition diagram appears in Figure 36-5. The solid circle with the arrow denotes the starting state of the state machine.4 The solid circle with the open circle around it is the final state of the machine.

Figure 36-5. Rectangle interaction state machine

image

GUI interactions are rife with FSMs. They are driven by the incoming events from the user. Those events cause changes in the state of the interaction.

Distributed Processing

Distributed processing is yet another situation in which the state of the system changes based on incoming events. For example, suppose that you had to transfer a large block of information from one node on a network to another. Suppose also that because network response time is precious, you need to chop up the block and send it as a group of small packets.

The state machine depicting this scenario is shown in Figure 36-6. It starts by requesting a transmission session, proceeds by sending each packet and waiting for an acknowledgment, and finishes by terminating the session.

Figure 36-6. Sending large block, using many packets

image

Conclusion

Finite state machines are underutilized. In many scenarios their use would help to create clearer, simpler, more flexible, and more accurate code. Making use of the STATE pattern and simple tools for generating the code from state transition tables can be of great assistance.

Bibliography

[GOF95] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.

[PLOPD1] James O. Coplien and Douglas C. Schmidt, Pattern Languages of Program Design, Addison-Wesley, 1995.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.121.160