State Machines in Domain Models
I was reminded today of Stateless, a little project that I’m quite fond of.
Stateless is a hierarchical state machine framework based on Simple State Machine for Boo, but configured using C# 3.0.
When I announced Stateless last year, I hardly even explained its purpose, let alone its tongue-in-cheek name. I think I owe it a better start in life!
If you’re doing heavy duty state-machine based programming (writing parsers, radiotherapy machines or flight control systems) then Stateless is probably not what you’re looking for.
If you’re doing domain-driven design, and anxious about the ugly nested ‘if’ and ‘switch’ statements building up around _state variables, Stateless can help.
State-based behaviour is awkward because despite careful programming, it is difficult to see how the states relate to each other. Adding and removing states is error-prone, repetition creeps in, and refactoring gets tricky.
There’s more than one way out. The State pattern can be one very elegant solution.
Another solution that works nicely is to create a declarative model of the states, transitions and associated actions, and have a state machine framework ‘run’ it for you. This is the approach enabled by Stateless.
The Telephone Call state chart is broken down into the following states and triggers:
You can use any types to represent states and triggers, but enumerations are convenient.
After creating an instance of StateMachine, each state is configured independently. Configuration for a state revolves around the triggers that the state can accept, the transitions that these triggers will cause (to new states) and the actions that will be performed when entering or leaving a state.
Once the state machine has been configured, the triggers can be ‘fired’. The side-effects from firing triggers drive the program.
You might wonder how this fits into a domain model. StateMachine certainly doesn’t belong on the public surface area of your domain model classes. It’s an implementation detail – like, for example, StringBuilder or Regex.
Users of the class interact with the state machine indirectly, by calling public methods on the domain object.
The state machine is configured to call two methods when entering and leaving the Connected state (see the configuration above.)
Stateless does all of the heavy lifting. For example, the state machine understands that because OnHold is a sub-state of Connected, the Connected entry/exit actions aren’t called when moving between these two states.
Now for a justification of the silly name…
Notice the way that the _state field belongs to the PhoneCall object, rather than the state machine? This allows the field to be easily mapped to a database column by NHibernate or your ORM of choice. The state machine reads and writes the _state value using the pair of lambdas provided to its constructor. In that sense you can almost say that the state machine itself is ‘stateless’. Almost.
There’s some more basic documentation and a source download at the site. I hope you’ll try Stateless and be pleasantly surprised by how expressive it can make your code. Search your domain model for “State” or “Status” and see what Stateless can do!
This post also comes with a challenge: Stateless is fairly simple and very hackable - if you’re a fluent-interface aficionado and think you can improve the configuration API, join the project!
Comments
Anonymous
April 18, 2009
great followup to the DSL Devcon Nick -- looking forward to exploring itAnonymous
April 18, 2009
The comment has been removedAnonymous
April 19, 2009
FYI, with the way you have it programmed, your _totalMinutes TimeSpan will not have the value you intended after a call to StopCallTimer. I submitted a documentation error on this recently to MS (actually, on DateTime, but it applies to TimeSpan as well, and presumably other structs), and hopefully this points it out even more. It claims to change the value of the instance, but of course, since it's a value-type, it actually just returns the value, not, "Changes the value of the instance." It's silly that anyone would write a piece of documentation to say that, and then at the very bottom buried in the remarks of the function it says that it doesn't change the ACTUAL value of the instance. Ugh.Anonymous
April 21, 2009
Cool implmentation. I am also maintaining an open-source state-machine project. It's a descendant of Leslie Sanford's state machine toolkit that was published on CodeProject. I took his project (with his permission of course) and added generics support and lots of other features. You can find my source here: https://sourceforge.net/projects/smtoolkit/ A good intro page can be found here: http://smtoolkit.wiki.sourceforge.net/ with links to the original project at CodeProject, and the added features.Anonymous
April 22, 2009
@Will, thanks for the link, will definitely check it out! @Kyle, good spotting, if I had a dollar for every time I messed that up :) @Omer, thanks also for the links, it looks like an interesting project.Anonymous
April 24, 2009
I think your concept is a not completely faithful to the state machine pattern. As I understand, a superstate contains substates, of which one of them is the initial substate. When entering a superstate, you end in the initial substate (unless you support history, and then you have some other options too). You can never be in a superstate without also being in one of its substates. In your implementation, the superstate is a state of its own, and you have to explicitly transition to one of its substates. In your example, to be faithful to the pattern, your Connected superstate should have a Talking substate which is the initial substate. You can transition from Talking to OnHold or vice versa. You should also not handle the HungUp trigger in the OnHold state: It enough to handle it in the Connected superstate.Anonymous
May 17, 2009
how to deal with the delays ...Anonymous
August 01, 2009
The comment has been removedAnonymous
August 02, 2009
Wow, thanks David! I'm really glad to hear that it's been useful.Anonymous
August 15, 2009
@Omer, sorry about the lagging response :) It's the implementer's decision whether to use the superstate as a valid state as I have done, or to require an 'initial substate' as you suggest. Stateless supports both of these styles, I've chosen the one that fits best in this particular example.Anonymous
February 24, 2010
Thanks for sharing this project! I've been going through the code samples and reading this document over and over again. I'm not sure why, but I was wondering if this could be used to control machines -- surely it looks like I can apply it to my project, but when you're controlling something like a machine, it seems as though all of the meat is going to be in the transitions between states, rather than really caring about the state itself. Does this make sense? I really like the design, so I'm going to give it a go, but I'd like to know if my interpretation of your suggestion regarding machine control makes sense. :)Anonymous
February 25, 2010
You need to change the example here to use Permit instead of Allow. :)Anonymous
August 07, 2012
David Robbins above mentioned driving this from a datasource. Any examples on doing this? Need to build a workflow engine and will never know ahead of time what the steps are. How can you programmatically add .Permits(trigger) to a StateMachine? How can you differentiate between Permit, PermitIf, OnExit, etc... from a data model and add to the Machine?