Nested SwitchCase Statements


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.




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