Search code examples
c#state-machinestateless-state-machine

State-machine - Stateless vs. traditional if-else code, hard to grasp the benefit


I've came across recently with a dirty if-else code, so I've looked for a refactor options and found recommendation on state-machine as an elegant replacement for dirty if-else code. But something is hard me to grasp: It looks that as client I have the responsibility to move the machine from one state to the other. Now, if there are 2 transitions options (depend on the result of work done in the current state) Do I need to use if-else also? If so, what the main benefit from that pattern? From my point of view the machine may do the transition automatically from the starting state

Before asking I've read the below, and it only strengthens my opinion:

Auto advancing state machine with Stateless

How to encapsulate .NET Stateless state machine

Statemachine that transitions to target state and fires transitions and states between?

In my example, I've an MarketPriceEvent which needs to be stored in Redis. Before stored it has to pass through validation path. The validation path states are:

  • Basic Validation
  • Comparison
  • Another comparison
  • Storing
  • Error auditing

The problem is that I've many decisions to make. For example: only if BasicValidation passed successfully I'd like to to move to Comparison. Now if Comparison succeeded i'd like to move to Storing, otherwise move to ErrorAuditing. So if we're going into code:

 _machine.Configure(State.Validate).PermitIf(Trigger.Validated, State.Compare1, () => isValid);

        _machine.Configure(State.Compare1).OnEntry(CompareWithResource1).
            PermitIf(Trigger.Compared, State.Store, () => isValid)
            .PermitIf(Trigger.Compared, State.Compare2, () => !isValid);

And in my client/wrapper code I'll write:

//Stay at Validate state
        var marketPriceProcessingMachine = new MarketPriceProcessingMachine();

        if (marketPriceProcessingMachine.Permitted(Trigger.Validated))
                       marketPriceProcessingMachine.Fire(Trigger.Validated);
        //else 
        // ...

In short, If I need to use if-else, What the benefit did I get from such State machine concept? If it's deterministic why it doesn't self move to the next state? If I'm wrong, What's the wrong?


Solution

  • One benefit of using a state machine is that you reduce the number of states an object can be in. I worked with someone who had 22 bool flags in a single class. There was a lot of if !(something && !somethingElse || !userClicked) …

    This sort of code is hard to read, hard to debug, hard to unit test and it's more or less impossible to reason about what the state of the class really is. 22 bool flags means that the class can be in over 4 million states. Try making unit tests for that...

    State machines can reduce the complexity of code, but it will almost always make the somewhat more complex at the beginning of a new project. However, in the long term I've found that the overall complexity ends up being overall lower. This is because it's easy to extend, and add more states, since the already defined states can be left alone.

    What I've found over the years is that OOP and state machines are often two aspects of the same. And I've also found that OOP is hard, and difficult to get 'right'.

    I think the state machine should not be visible to the outside of an object, including its triggers. You most likely want to have a public readonly state property.

    I design the classes in such a way that the caller can not directly change the state, or let the caller call Fire method directly. Instead I use methods that are verbs that are actions, like Validate().

    Your work flow needs conditionals, but you have some freedom of where to put them. I would suggest separating the business logic from the state machine configuration. I think this makes the state machine easier to read.

    How about something like this:

    namespace ConsoleApp1
    {
        using Stateless;
        using System;
    
        class Program
        {
            static void Main(string[] args)
            {
                Console.WriteLine("Press Q to stop validating events");
                ConsoleKeyInfo c;
    
                do
                {
                    var mpe = new MarketPriceEvent();
                    mpe.Validate();
                    c = Console.ReadKey();
    
                } while (c.Key != ConsoleKey.Q);
            }
        }
    
        public class MarketPriceEvent
        {
            public void Validate()
            {
                _machine.Fire(Trigger.Validate);
            }
    
            public enum State { Validate, Compare2, ErrorAuditing, Compare1, Storing }
            private enum Trigger { Validate, CompareOneOk, CompareTwoOk, Error, }
    
            private readonly StateMachine<State, Trigger> _machine;
            public MarketPriceEvent()
            {
                _machine = new StateMachine<State, Trigger>(State.Validate);
    
                _machine.Configure(State.Validate)
                    .Permit(Trigger.Validate, State.Compare1);
    
                _machine.Configure(State.Compare1)
                    .OnEntry(DoEventValidation)
                    .Permit(Trigger.CompareOneOk, State.Compare2)
                    .Permit(Trigger.Error, State.ErrorAuditing);
    
                _machine.Configure(State.Compare2)
                    .OnEntry(DoEventValidationAgainstResource2)
                    .Permit(Trigger.CompareTwoOk, State.Storing)
                    .Permit(Trigger.Error, State.ErrorAuditing);
    
                _machine.Configure(State.Storing)
                    .OnEntry(HandleStoring);
    
                _machine.Configure(State.ErrorAuditing)
                    .OnEntry(HandleError);
            }
    
            private void DoEventValidation()
            {
                // Business logic goes here
                if (isValid())
                    _machine.Fire(Trigger.CompareOneOk);
                else
                    _machine.Fire(Trigger.Error);
            }
    
            private void DoEventValidationAgainstResource2()
            {
                // Business logic goes here
                if (isValid())
                    _machine.Fire(Trigger.CompareTwoOk);
                else
                    _machine.Fire(Trigger.Error);
            }
            private bool isValid()
            {
                // Returns false every five seconds...
                return (DateTime.UtcNow.Second % 5) != 0;
            }
    
            private void HandleStoring()
            {
                Console.WriteLine("Awesome, validation OK!");
            }
    
            private void HandleError()
            {
                Console.WriteLine("Oh noes, validation failed!");
            }
    
        }
    }