2

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?

Roni
  • 369
  • 1
  • 7
  • 22
  • 2
    A state machine is used to reduce the number of test. Suppose you had an application the for people and you had two criteria sex and age and people lived to be 100 years. You would have 200 if statements for every combination. Now suppose you only need to group people in age groups of ten years. You would have only 20 states. So a state machine is used to reduce the number of tests from 200 to 20. I usually implement my state machines use a Select instead of If/Else and include a default to make sure I do not leave anything out of my groupings. – jdweng May 12 '19 at 10:40
  • 1
    @jdweng Sorry, I couldn't catch your explanation. If you have 200 combinations and every combination has its unique behavior you can't group it also in state machine, otherwise you can do that in shorter `if-else` code too !? Also if you can please expand the explanation of your sentence "_I usually implement my state machines use a Select instead of If/Else_" – Roni May 12 '19 at 11:22
  • Exactly!!! A state machine is meant to group items with the same response (behavior). That is the answer to your first question the benefit of a state machine. The second question of self moving. A state machine needs to know next state. There is no such thing as "self move" it must be defined. The move to next state can be made in different places in the code. Implementation of state machine is left to OP. – jdweng May 12 '19 at 13:43
  • Read through the example [here](https://github.com/scottctr/NStateManager/wiki/Quick-Start) and look at how payments are applied and how the state machine determines if the sale should go to ChangeDue or Complete by applying the rules in priority order. IMHO, the resulting code from this state machine framework solution is much cleaner than the equivalent if/else solution. I'm not familiar enough will all of your rules to guarantee this is the right solution for you though. – scottctr May 12 '19 at 13:55

1 Answers1

7

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!");
        }

    }
}
HenningNT
  • 183
  • 8
  • 2
    Thanks! Exactly what I've looked for - how to handle branch at the end of state (like: Error/Success). your code helped me so much: `private void DoEventValidationAgainstResource2() { // Business logic goes here if (isValid()) _machine.Fire(Trigger.CompareTwoOk); else _machine.Fire(Trigger.Error); }` – Roni May 13 '19 at 08:13
  • 2
    I'm glad I could be of assistance! :-) – HenningNT May 13 '19 at 08:37