© Dmitri Nesteruk 2020
D. NesterukDesign Patterns in .NET Core 3https://doi.org/10.1007/978-1-4842-6180-4_22

22. State

Dmitri Nesteruk1  
(1)
St. Petersburg, c.St-Petersburg, Russia
 

I must confess: my behavior is governed by my state. If I didn’t get enough sleep, I’m going to be a bit tired. If I had a drink, I wouldn’t get behind the wheel. All of these are states, and they govern my behavior: how I feel, what I can and cannot do.

I can, of course, transition from one state to another. I can go get a coffee, and this will take me from sleepy to alert (I hope!). So we can think of coffee as a trigger that causes a transition of yours truly from sleepy to alert. Here, let me clumsily illustrate it for you:1
        coffee
sleepy --------> alert

So, the State design pattern is a very simple idea: state controls behavior, state can be changed, the only thing which the jury is out on is who triggers the change from one state to another.

There are two ways in which we can model states:
  • States are actual classes with behaviors, and these behaviors cause the transition from this state to another. In other words, a state’s members are the options in terms of where we can go from that state.

  • States and transitions are just enumerations. We have a special component called a state machine that performs the actual transitions.

Both of these approaches are viable, but it’s really the second approach that is the most common. We’ll take a look at both of them, but I must warn that I’ll glance over the first one, since this isn’t how people typically do things.

State-Driven State Transitions

We’ll begin with the most trivial example out there: a light switch that can only be in the on and off states. The reason why we are choosing such a simple domain is that I want to highlight the madness (there’s no other word for it) that a classic implementation of State brings, and this example is simple enough to do so without generating pages of code listings.

We are going to build a model where any state is capable of switching to some other state: this reflects the “classic” implementation of the State design pattern (as per GoF book). First, let’s model the light switch. All it has is a state and some means of switching from one state to another:
public class Switch
{
  public State State = new OffState();
}
This all looks perfectly reasonable; we have a switch that’s in some state (either on or off). We can now define the State which, in this particular case, is going to be an actual class.
public abstract class State
{
  public virtual void On(Switch sw)
  {
    Console.WriteLine("Light is already on.");
  }
  public virtual void Off(Switch sw)
  {
    Console.WriteLine("Light is already off.");
  }
}

This implementation is far from intuitive, so much so that we need to discuss it slowly and carefully, because from the outset, nothing about the State class makes sense.

While the State is abstract (meaning you cannot instantiate it), it has nonabstract members that allow the switching from one state to another. This… to a reasonable person, it makes no sense. Imagine the light switch: it’s the switch that changes states. The state itself isn’t expected to change itself, and yet it appears this is exactly what it does.

Perhaps most bewildering, though, the default behavior of State.On()/Off() claims that we are already in this state! Note that these methods are virtual. This will come together, somewhat, as we implement the rest of the example.

We now implement the On and Off states:
public class OnState : State
{
  public OnState()
  {
    Console.WriteLine("Light turned on.");
  }
  public override void Off(Switch sw)
  {
    Console.WriteLine("Turning light off...");
    sw.State = new OffState();
  }
}
// similarly for OffState

The constructor of each state simply informs us that we have completed the transition. But the transition itself happens in OnState.Off() and OffState.On() . That is where the switching happens.

We can now complete the Switch class by giving it methods to actually switch the light on and off:
public class Switch
{
  public State State = new OffState();
  public void On()  { State.On(this); }
  public void Off() { State.Off(this); }
}
So, putting it all together, we can run the following scenario:
LightSwitch ls = new LightSwitch(); // Light turned off
ls.On();  // Switching light on...
          // Light turned on
ls.Off(); // Switching light off...
          // Light turned off
ls.Off(); // Light is already off
Here is an illustrated transition from OffState to OnState:
         LightSwitch.On() -> OffState.On()
OffState------------------------------------>OnState
On the other hand, the transition from OnState to OnState uses the base State class, the one that tells you that you are already in that state:
         LightSwitch.On() -> State.On()
OnState --------------------------------> OnState
Let me be the first to say that the implementation presented here is terrible. While being a nice demonstration of OOP equilibristics, it is an unreadable, unintuitive mess that goes against everything we learn about both OOP generally and design patterns in particular, specifically:
  • A state typically does not switch itself.

  • A list of possible transitions should not appear all over the place; it’s best to keep it in one place (SRP).

  • There is no need to have actual classes modeling states unless they have class-specific behaviors; this example could be reduced to something much simpler.

Maybe we should have been using enums to begin with?

Handmade State Machine

Let’s try to define a state machine for a typical phone conversation.

First of all, we’ll describe the states of a phone:
public enum State
{
  OffHook,
  Connecting,
  Connected,
  OnHold
}
We can now also define transitions between states, also as an enum:
public enum Trigger
{
  CallDialed,
  HungUp,
  CallConnected,
  PlacedOnHold,
  TakenOffHold,
  LeftMessage
}

Now, the exact rules of this state machine, that is, what transitions are possible, need to be stored somewhere. Here is a UML State Machine diagram showing what kind of transitions we want:

../images/476082_2_En_22_Chapter/476082_2_En_22_Figa_HTML.jpg
Let’s use a dictionary of state-to-trigger/state pairs:
private static Dictionary<State, List<(Trigger, State)>> rules
    = new Dictionary<State, List<(Trigger, State)>>() { /* todo */ }

This is a little clumsy, but essentially the key of the dictionary is the State we’re moving from, and the value is a list of Trigger-State pairs representing possible triggers while in this state and the state you move into when you use the trigger.

Let’s initialize this data structure:
private static Dictionary<State, List<(Trigger, State)>> rules
  = new Dictionary<State, List<(Trigger, State)>>
  {
    [State.OffHook] = new List<(Trigger, State)>
    {
      (Trigger.CallDialed, State.Connecting)
    },
    [State.Connecting] = new List<(Trigger, State)>
    {
      (Trigger.HungUp, State.OffHook),
      (Trigger.CallConnected,  State.Connected)
    },
    // more rules here
  };
We also need a starting (current) state, and we can also add an exit (terminal) state if we want the state machine to stop executing once that state is reached:
State state = State.OffHook, exitState = State.OnHook;

So in the preceding line, we start out with the OffHook state (when you’re ready to make the call) and the exit state is when the phone is placed OnHook and the call is finished.

Having made this, we don’t necessarily have to build a separate component for actually running (we use the term orchestrating) a state machine. For example , if we wanted to build an interactive model of the telephone, we could do it like this:
do
{
  Console.WriteLine($"The phone is currently {state}");
  Console.WriteLine("Select a trigger:");
  for (var i = 0; i < rules[state].Count; i++)
  {
    var (t, _) = rules[state][i];
    Console.WriteLine($"{i}. {t}");
  }
  int input = int.Parse(Console.ReadLine());
  var (_, s) = rules[state][input];
  state = s;
} while (state != exitState);
Console.WriteLine("We are done using the phone.");

The algorithm is fairly obvious: we let the user select one of the available triggers on the current state, and provided the trigger is valid, we transition to the right state by using that rules dictionary that we created earlier.

If the state we’ve reached is the exit state, we just jump out of the loop. Here’s a sample interaction with the program:
The phone is currently OffHook
Select a trigger:
0. CallDialed
0
The phone is currently Connecting Select a trigger:
0.HungUp
1.CallConnected
1
The phone is currently Connected
Select a trigger:
0.LeftMessage
1.HungUp
2.PlacedOnHold
2
The phone is currently OnHold
Select a trigger:
0.TakenOffHold
1.HungUp
1
We are done using the phone.

This hand-rolled state machine’s main benefit is that it is very easy to understand: states and transitions are ordinary enumerations, the set of transitions is defined in a Dictionary, the start and end states are simple variables. I’m sure you’ll agree this is much easier to understand than the example we started the chapter with.

Switch-Based State Machine

In our exploration of state machines, we have progressed from the needlessly complicated Classic example where states are represented by classes to a handcrafted example where states are represented as enumerations, and now we shall experience one final step of degradation, as we stop using dedicated data types for states altogether.

But our simplifications won’t end there: instead of jumping from one method call to another, we’ll confine ourselves to an infinitely recurring switch statement where state will be examined and transitions will happen by virtue of the state changing.

The scenario I want you to consider is a combination lock. The lock has a four-digit code (e.g., 1234) that you enter one digit at a time. As you enter the code, if you make a mistake, you get the “FAILED” output, but if you enter all digits correctly, you get “UNLOCKED” instead and you exit the state machine.

The entire scenario can fit into a single listing:
string code = "1234";
var state = State.Locked;
var entry = new StringBuilder();
while (true)
{
  switch (state)
  {
    case State.Locked:
      entry.Append(Console.ReadKey().KeyChar);
      if (entry.ToString() == code)
      {
        state = State.Unlocked;
        break;
      }
      if (!code.StartsWith(entry.ToString()))
      {
        // the code is blatantly wrong
        state = State.Failed;
      }
      break;
    case State.Failed:
      Console.CursorLeft = 0;
      Console.WriteLine("FAILED");
      entry.Clear();
      state = State.Locked;
      break;
    case State.Unlocked:
      Console.CursorLeft = 0;
      Console.WriteLine("UNLOCKED");
      return;
  }
}

As you can see, this is very much a state machine, albeit one that lacks any structure. You couldn’t examine it from the top level and be able to tell what all the possible states and transitions are. It is not clear, unless you really examine the code, how the transitions happen – and we’re lucky there are no goto statements here to make jumps between the cases!

This switch-based state machine approach is viable for scenarios with very small numbers of states and transitions. It loses out on structure, readability, and maintainability, but can work as a quick patch if you do need a state machine quickly and are too lazy to make enum cases.

Overall, this approach does not scale and is difficult to manage, so I would not recommend it in production code. The only exception would be if such a machine was made using code generation on the basis of some external model.

Encoding Transitions with Switch Expressions

The switch-based state machine may be unwieldy, but that’s partly due to the way information about states and transitioned is structured (because it’s not). But there is a different kind of switch – a switch statement (as opposed to expression) that, thanks to pattern matching, allows us to neatly define state transitions.

Okay, time for a simple example. You’re on a hunt for treasure and find a treasure chest that you can open or close… unless it’s locked, in which case the situation is a bit more complicated (you need to have a key to lock or unlock a chest). Thus, we can encode the states and possible transitions as follows:
enum Chest
{
  Open, Closed, Locked
}
enum Action
{
  Open, Close
}
With this definition, we can write a method called Manipulate that takes us from one state to another. The general rules of chest operation are as follows:
  • If the chest is locked, you can only open it if you have the key.

  • If the chest is open and you close it while having the key, you lock it.

  • If the chest is open and you don’t have the key, you just close it.

  • A closed (but not locked) chest can be opened whether you have the key or not.

The set of possible transitions can be encoded right in the structure of the pattern matching expression. Without further ado, here it is:
static Chest Manipulate(Chest chest,
  Action action, bool haveKey) =>
  (chest, action, haveKey) switch
  {
    (Chest.Closed, Action.Open, _) => Chest.Open,
    (Chest.Locked,  Action.Open, true) => Chest.Open,
    (Chest.Open, Action.Close, true) => Chest.Locked,
    (Chest.Open, Action.Close, false) => Chest.Closed,
  _ => chest
};
This approach has a number of advantages and disadvantages. The advantages are that
  • This state machine is easy to read

  • Guard conditions such as haveKey are easy to incorporate, and fit nice into pattern matching

There are disadvantages, too:
  • The formal set of rules for this state machine is defined in a way that cannot be extracted. There’s no data store where the rules are kept, so you cannot generate a report or a diagram, or run any verification checks beyond those that are done by the compiler (it checks for exhaustiveness).

  • If you need any behavior, such as state entry or exit behavior, this cannot be done easily in a switch expression – you would need to define a good old-fashioned method with a switch statement in it.

To sum up, this approach is great for simple state machines because it results in very readable code. But it’s not exactly an “enterprise” solution.

State Machines with Stateless

While hand-rolling a state machine works for the simplest of cases, you probably want to leverage an industrial-strength state machine framework. That way, you get a tested library with a lot more functionality. It’s also fitting because we need to discuss additional state machine–related concepts, and implementing them by hand is rather tedious.

Before we move on to the concepts I want to discuss, let us first of all reconstruct our previous phone call example using Stateless.2 Assuming the existence of the same enumerations State and Trigger as before, the definition of a state machine is very simple:
var call = new StateMachine<State, Trigger>(State.OffHook);
phoneCall.Configure(State.OffHook)
  .Permit(Trigger.CallDialed, State.CallConnected);
// and so on, then, to cause a transition, we do
call.Fire(Trigger.CallDialed); // call.State is now State.CallConnected

As you can see, Stateless’ StateMachine class is a builder with a fluent interface. The motivation behind this API design will become apparent as we discuss the different intricacies of Stateless.

Types, Actions, and Ignoring Transitions

Let’s talk about the many features of Stateless and state machines generally.

First and foremost, Stateless supports states and triggers of any .NET type – it’s not constrained to enums. You can use strings, numbers, anything you want. For example, a light switch could use a bool for states (false = off, true = on); we’ll keep using enums for triggers. Here is how one would implement the LightSwitch example:
enum Trigger { On, Off }
var light = new StateMachine<bool, Trigger>(false);
light.Configure(false)            // if the light is off...
  .Permit(Trigger.On, true)       // we can turn it on
  .Ignore(Trigger.Off);           // but if it's already off we do nothing
// same for when the light is on
light.Configure(true)
  .Permit(Trigger.Off, false)
  .Ignore(Trigger.On)
  .OnEntry(() => timer.Start())
  .OnExit(() => timer.Stop());   // calculate time spent in this state
light.Fire(Trigger.On);   // Turning light on
light.Fire(Trigger.Off);  // Turning light off
light.Fire(Trigger.Off);  // Light is already off!

There are a few interesting things worth discussing here. First of all, this state machine has actions – things that happen as we enter a particular state. These are defined in OnEntry() , where you can provide a lambda that does something; similarly, you could invoke something at the moment the state is exited using OnExit() . One use of such transition actions would be to start a timer when entering a transition and stop it when exiting one, which could be used for tracking the amount of time spent in each state. For example, you might want to measure the time the light stays on for purposes of verifying electricity costs.

Another thing worth noting is the use of Ignore() builder methods. This basically tells the state machine to ignore the transition completely: if the light is already off, and we try to switch it off (as in the last line of the preceding listing), we instruct the state machine to simply ignore it, so no output is produced in that case.

Why is this important? Because if you forget to Ignore() this transition or fail to specify it explicitly, Stateless will throw an InvalidOperationException:

“No valid leaving transitions are permitted from state ‘False’ for trigger ‘False’. Consider ignoring the trigger.

Reentrancy Again

Another alternative to the “redundant switching” conundrum is Stateless’ support for reentrant states . To replicate the example at the start of this chapter, we can configure the state machine so that in the case of reentry into a state (meaning that we transition, say, from false to false), an action is invoked. Here is how one would configure it:
var light = new StateMachine<bool, Trigger>(false);
light.Configure(false)        // if the light is off...
  .Permit(Trigger.On, true)   // we can turn it on
  .OnEntry(transition =>
  {
    if (transition.IsReentry)
      WriteLine("Light is already off!");
    else
      WriteLine("Turning light off");
  })
  .PermitReentry(Trigger.Off);
// same for when the light is on
light.Fire(Trigger.On);  // Turning light on
light.Fire(Trigger.Off); // Turning light off
light.Fire(Trigger.Off); // Light is already off!

In the preceding listing, PermitReentry() allows us to return to the false (off ) state on a Trigger.Off trigger. Notice that in order to output a corresponding message to the console, we use a different lambda: one that has a Transition parameter . The parameter has public members that describe the transition fully. This includes Source (the state we’re transitioning from), Destination (the state we’re going to), Trigger (what caused the transition), and IsReentry, a Boolean flag that we use to determine if this is a reentrant transition.

Hierarchical States

In the context of a phone call, it can be argued that the OnHold state is a substate of the Connected state, implying that when we’re on hold, we’re also connected. Stateless lets us configure the state machine like this:
phoneCall.Configure(State.OnHold)
    .SubstateOf(State.Connected)
    // etc.

Now, if we are in the OnHold state, phoneCall.State will give us OnHold, but there’s also a phoneCall.IsInState(State) method that will return true when called with either State.Connected or State.OnHold.

More Features

Let’s talk about a few more features related to state machines that are implemented in Stateless.
  • Guard clauses allow you to enable and disable transitions at will by calling PermitIf() and providing bool-returning lambda functions, for example:

phoneCall.Configure(State.OffHook)
  .PermitIf(Trigger.CallDialled, State.Connecting, () => IsValidNumber)
  .PermitIf(Trigger.CallDialled, State.Beeping, () =>!IsValidNumber);
  • Parameterized triggers are an interesting concept. Essentially, you can attach parameters to triggers such that, in addition to the trigger itself, there’s also additional information that can be passed along. For example, if a state machine needs to notify a particular employee, you can specify an email to be used for notification:

var notifyTrigger = workflow.SetTriggerParameters<string>(Trigger.Notify);
workflow.Configure(State.Notified)
  .onEntryFrom(assignTrigger, email => SendEmail(email));
workflow.Fire(notifyTrigger, "[email protected]");
  • External storage is a feature of Stateless that lets you store the internal state of a state machine externally (e.g., in a database) instead of using the StateMachine class itself. To use it, you simply define the getter and setter methods in the StateMachine constructor:

var stateMachine = new StateMachine<State, Trigger>(
  () => database.ReadState(),
  s => database.WriteState(s));
  • Introspection allows us to actually look at the table of triggers that can be fired from the current state through PermittedTriggers property .

This is far from an exhaustive list of features that Stateless offers, but it covers all the important parts.

Summary

As you can see, the whole business of state machines extends way beyond simple transitions: it allows a lot of complexity to handle the most demanding business cases. Let us recap some of the state machine features we’ve discussed:
  • State machines involve two collections: states and triggers. States model the possible states of the system and triggers transition us from one state to another. You are not limited to enumerations: you can use ordinary data types.

  • Attempting a transition that’s not configured will result in an exception.

  • It is possible to explicitly configure entry and exit actions for each state.

  • Reentrancy can be explicitly allowed in the API, and furthermore, you can determine whether or not a reentry is occurring in the entry/exit action.

  • Transitions can be turned on an off through guard conditions. They can also be parameterized.

  • States can be hierarchical, that is, they can be substates of other states. An additional method is then required to determine whether you’re in a particular (parent) state.

While most of these might seem like overengineering, these features provide great flexibility in defining real-world state machines.

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

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