5

TL;DR:

Where / how should a state machine framework determine what the next state should be? Or, is this in fact out of scope for state machines, which are actually only for tracking the current state and validating whether a requested transition is allowed?


Background & details:

Consider a simple magazine article publishing workflow. The following diagram shows a basic, conceptual understanding of the process, which we'd like to turn into code (in this case using Stateless). It covers the basic, "happy path" of publication, plus a few possible issues:

┌───┬──────────────────────────────────────────┐
│ W │    ┌───────────┐                         │
│ r │    │           │                         │
│ i │    │   Write   ◄────────┐                │
│ t │    │           │        │                │
│ e │    └───────────┘        │                │
│ r │          │              │                │
├───┼────────Submit───────────│────────────────┤
│   │          │              │                │
│   │    ┌─────▼─────┐        │                │
│   │    │           │        │                │
│   │    │  Review   ◄──────┬─│──────────┐     │
│   │    │           │      │ │          │     │
│   │    └───────────┘      │ │          │     │
│   │          │            │ │          │     │
│   │      ____▼___         │ │          │     │
│ E │     /        \        │ │          │     │
│ d │    /  Grammar \───No──│─┘          │     │
│ i │    \    OK?   /       │            │     │
│ t │     \________/        │         Resolve  │
│ o │          │           Date          │     │
│ r │         Yes         Passed         │     │
│   │          │            │            │     │
│   │      ____▼___         │            │     │
│   │     /        \        │            │     │
│   │    /   Libel  \────Yes│────────┐   │     │
│   │    \   Risk?  /       │        │   │     │
│   │     \________/        │        │   │     │
│   │          │        ┌───│───┐    │   │     │
│   │          No       │ Defer │    │   │     │
│   │          │        └───▲───┘    │   │     │
│   │      ____▼___         │        │   │     │
│   │     /        \        │        │   │     │
│   │    / Embargo? \─Yes───┘        │   │     │
│   │    \          /                │   │     │
│   │     \________/                 │   │     │
│   │          │                     │   │     │
├───┼──────────No────────────────────│───│─────┤
│ L │          │                     │   │     │
│ e │          │               ┌─────▼───│─┐   │
│ g │          │               │   Legal   │   │
│ a │          │               │  Review   │   │
│ l │          │               └───────────┘   │
├───┼──────────│───────────────────────────────┤
│ P │          │                               │
│ r │    ┌─────▼───────┐                       │
│ i │    │             │                       │
│ n │    │  Print it!  │                       │
│ t │    │             │                       │
│ e │    └─────────────┘                       │
│ r │                                          │
└───┴──────────────────────────────────────────┘

My question is around how to think about the state transitions, specifically the transition after the editor has completed the review.

One way to do it (A) would be to put the onus on the user to choose the appropriate transition; so in this case the editor would have separate buttons called Revert to Check Spelling, Refer for Legal Review and Defer Publication. These would be wired up to corresponding methods on the Article object, which internally call .Fire(...) on a _stateMachine with the respective Triggers, per the recommended approach.

There are at least two disadvantages to that however. First, it increases cognitive load on the user (marginally, in this case, but stay with me for the sake of the example). She shouldn't have to choose what to do, she should just be able to do all of the data input in one go, on a form like this:

┌─────────────────┬──────────────────────┐
│ Spelling OK?    │ Yes [ ]   No [ ]     │
│ Libel Risk?     │ Yes [ ]   No [ ]     │
│ Embargo?        │ Date [ dd/mm/yyyy ]  │
└─────────────────┴──────────────────────┘

And then the application should decide what to do. That would also prevent the user from making the wrong choice: it's easier to answer factual questions about the content than decide the correct course of action given potentially many inputs (imagining a more complex example).

The second disadvantage is that the review could be interrupted prematurely: whenever one issue is found and the corresponding action triggered, the editor doesn't get to continue the review to potentially find other issues. Once the first issue is resolved and the article comes back for review again, it in practice starts again from scratch.

Another recommendation (B) I've seen is to model more states: a Grammar Issue state, a Contains Libel state and so on. However, that comes back to the same problem: a user would have to trigger the transition to those states individually (assuming not all of these issues are determinable automatically by e.g. a grammar checker or such).

Furthermore, this feels like moving away from our relatively clean model of the world, or at least from the supposed cleanliness offered by a state machine library. Imagine the proliferation of states in a more complex example, with the user entering a greater number of variables. I was looking forward to using Stateless' export to DOT graph functionality to achieve, as they say, the code as the authoritative source and diagrams as by-products. However, the value of that for stakeholder communication would diminish if the outputs are full of fairly unintuitive "states" which no longer correspond to commonly understood "phases" of the workflow.

That seems to leave me with (C), the option of giving the editor a Submit function which just contains a bunch of if statements to determine the correct next trigger to .Fire(). On one hand that feels like backsliding from the advantages which a state machine framework is meant to provide (that's no slight against Stateless—the purpose of this question is to determine whether I'm Holding It Wrong). On the other hand, I recognise that I'm still getting the benefits of its structure in relation to the other states, with simpler transitions.


There is another SO question which provides a simpler example:

Let's take the simple ATM example. If user presses "Confirm" AND PIN is correct, go to State 2. If user presses "Confirm" AND PIN is not correct, go to State 3.

That question seems to be asking more about modelling / notation than implementation, but this example is analogous to my example of the editor pressing Submit on her completed form.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
Ryan Jendoubi
  • 192
  • 11
  • I've got to believe that the final question in your post is answered by the design of the Stateless package you are using. It must explain where to position transition logic and state logic. I am not familiar with the package, so can't point you to any examples. –  Jan 08 '18 at 05:03
  • Who ever said that a state machine only transitions using binary decisions? I also feel that you are trying to couple your implementation with the interface that you give the user. The way you present the data to the editor does not have to correlate to the underlying state machine. It's perfectly reasonable to have a single review form with a submit button that activates several transitions in the underlying mechanism. You can also accompany a certain transition, say bad grammar detected, with a list of the grammatical mistakes that must all be fixed. – o_weisman Jan 08 '18 at 07:01
  • Hi Ryan. I'm working on a new project with exactly the same challenge. Mind sharing how you solved it in the end? – Francois Botha Aug 27 '21 at 08:41
  • 1
    Hi @FrancoisBotha. Our solution actually ended up looking most like Option A above (separate action buttons), but we avoided the cognitive load on the user by using [Stateless's Guard Clauses feature](https://github.com/dotnet-state-machine/stateless#guard-clauses). So the user would indeed fill in a form, as shown above, and then instead of a generic Submit button, guard clauses ensure that only valid transition options are presented. So we did manage the logic "natively" in the state machine lib (don't know if all FSM libs have this feature). Hope this helps? And sorry for the late reply. – Ryan Jendoubi Oct 06 '21 at 00:56

1 Answers1

1

As a suggestion, I think you could consider whether self-referential transitions would be useful in your editor machine. For example the the grammar state would have an internal list of tagged errors that might be empty or non-empty. No transition occurs, but you can think of it as an infinite transition to the current state. Transition out of the grammar state occurs with a trigger event such as your Submit, and at that point the internal list is tested to determine the next state.