Project Description
Ckknight.StateMachine is a .NET library that provides a flexible and efficient system for constructing, managing, and using Finite-State Machines.

The design uses a fluent API for building the StateMachine, and a typical .NET calling mechanism for using the constructed machines.

The user-oriented flow works as such:
  • You create a StateMachineBuilder. This can be done in a static context, and is recommended to do so if you plan on constructing many state machines that will attach to specific objects. It is not recommended to expose the builder outside of a local scope.
  • You configure the builder and all its states in the same context that you created it.
  • You create a factory from the builder. This can be exposed to the outside, as it is both immutable and thread-safe. If any further changes happen to the builder, they will not be reflected in the factory.
  • You generate zero or more instances of the state machine using the factory. This can be done at any point in time as long as a reference to the factory is kept.

If you plan on only having a singleton StateMachine, there is (obviously) no reason to expose to factory to anyone or keep a reference to it. The garbage collector will properly clean up the builder and factory as long as no more references to them are kept. Even if you hold a reference to just the factory, the builder will be properly garbage-collected.

StateMachines (and the associated builder) have three type parameters: TState, TTrigger, and TModel. In cases where you don't attach specific objects to the machine, TModel can be ignored and left as System.Object. There are no type restrictions on any of these type parameters. The typical use case will be to have TState and TTrigger be distinct enums, though nothing is stopping you from using strings, custom objects, or anonymous objects. The only prevention is that specified states and triggers must be non-null.

Door example:
    enum State { Opened, Closed };
    enum Trigger { OpenDoor, CloseDoor };
    var factory = StateMachineBuilder.Create<State, Trigger>()
        .Configure(State.Opened)
            .Permit(Trigger.CloseDoor, State.Closed)
            .End()
        .Configure(State.Closed)
            .Permit(Trigger.OpenDoor, State.Opened)
            .End()
        .CreateFactory();

    var machine = factory.Create(State.Closed);
    machine.Fire(Trigger.OpenDoor);
    Assert(machine.State == State.Opened);

    machine.Fire(Trigger.OpenDoor); // whoops, that's an error

    machine.Fire(Trigger.CloseDoor);
    Assert(machine.State == State.Closed);


ORM Example:
    // Often, we deal with objects we from a database or ORM system, and we
    // can get the state machine system to work with this very nicely
    // rather than setting an initial state when generating the machine,
    // we can specify a custom getter and setter.

    enum State { Normal, Deleted }
    enum Trigger { SoftDelete, Undelete }

    public class MyModel
    {
        public MyModel()
        {
            // also, rather than storing as a field, you could make a property and just generate
            // a new StateMachine each time. It should be efficient enough.
            _stateMachine = _stateMachineFactory.Generate(
                this, // will be on _stateMachine.Model, handy in dynamic cases. (Optional)
                () => (State)this.State, // custom state getter
                value => this.State = (State)value // custom state setter
            );
        }
        
        public int State { get; set; } // let's assume this is populated by the ORM
        
        private readonly StateMachine<State, Trigger, MyModel> _stateMachine;
        
        private static readonly StateMachineFactory<State, Trigger, MyModel> _stateMachineFactory =
            StateMachineBuilder.Create<State, Trigger, MyModel>()
                .Configure(State.Normal)
                    .Permit(Trigger.SoftDelete, State.Deleted)
                    .End()
                .Configure(State.Deleted)
                    .Permit(Trigger.Undelete, State.Normal)
                    .OnEnter(t => {
                        MyModel model = t.Model;
                        model.DeleteChildren();
                    })
                    .OnExit(t => {
                        MyModel model = t.Model;
                        model.UndeleteChildren();
                    })
                    .End()
                .CreateFactory();
    }


Triggers that have parameters:
    // Occasionally, triggers by themselves aren't quite useful enough or you want extra data passed in
    // Up to 4 parameters can be specified.

    private enum State { Flying, Spinning, Dead }
    private enum Trigger { HopOnTrampoline, GetHitByEnemy }
    var machine = StateMachineBuilder.Create<State, Trigger>()
        .SetTriggerParameters<Trampoline>(Trigger.HopOnTrampoline)
        .SetTriggerParameters<Enemy>(Trigger.GetHitByEnemy)
        .Configure(State.Flying)
            .Permit(Trigger.HopOnTrampoline, State.Spinning)
            .Permit(Trigger.GetHitByEnemy, State.Dead)
            .OnExitTo<Trampoline>(Trigger.HopOnTrampoline, t =>
            {
                Trampoline trampoline = t.Item1;
            })
            .End()
        .Configure(State.Spinning)
            .Permit(Trigger.HopOnTrampoline, State.Flying)
            .Permit(Trigger.GetHitByEnemy, State.Dead)
            .OnExitTo<Trampoline>(Trigger.HopOnTrampoline, t =>
            {
                Trampoline trampoline = t.Item1;
            })
            .End()
        .Configure(State.Dead)
            .OnEnterFrom<Enemy>(Trigger.GetHitByEnemy, t =>
            {
                Enemy enemy = t.Item1;
            })
            .End()
        .CreateFactory()
        .Generate(State.Flying);
    
    machine.Fire(Trigger.HopOnTrampoline); // error, no parameter passed in.
    machine.Fire(Trigger.HopOnTrampoline, someTrampoline, someEnemy); // error, too many parameters
    machine.Fire(Trigger.HopOnTrampoline, someEnemy); // error, bad parameter type
    machine.Fire(Trigger.HopOnTrampoline, someTrampoline);


Superstates and Substates:
    private enum State { Alive, Eating, Dead }
    private enum Trigger { Eat, StopEating, Die }

    var machine = StateMachineBuilder.Create<State, Trigger>()
        .Configure(State.Alive)
            .Permit(Trigger.Eat, State.Eating)
            .Permit(Trigger.Die, State.Dead)
            .End()
        .Configure(State.Eating)
            .Substate(State.Alive)
            .Permit(Trigger.StopEating, State.Alive)
            .End()
        .CreateFactory()
        .Generate(State.Alive);

    Assert(machine.IsInState(State.Alive));
    Assert(!machine.IsInState(State.Eating));
    machine.Fire(Trigger.Eat); // Eating
    Assert(machine.IsInState(State.Alive)); // it's still alive, since Eating's parent is Alive
    Assert(machine.IsInState(State.Eating));
    machine.Fire(Trigger.StopEating); // Alive
    machine.Fire(Trigger.Eat); // Eating
    machine.Fire(Trigger.Eat); // still Eating
    machine.Fire(Trigger.Die); // Dead


Handling unhandled triggers:
    // if you try to transition using a trigger that is not ignored or
    // permitted in some way, it will normally throw an ArgumentException. In
    // order to prevent this or handle it yourself, you can do the following:
    
    private enum State { Alive, Dead }
    private enum Trigger { Die, Respawn }

    var machine = StateMachineBuilder.Create<State, Trigger>()
        .Configure(State.Alive)
            .Permit(Trigger.Die, State.Dead)
            .End()
        .Configure(State.Alive)
            .Permit(Trigger.Respawn, State.Alive)
            .End()
        .OnUnhandledTrigger(t => LogMessage("Unhandled trigger"))
        .CreateFactory()
        .Generate(State.Alive);

    machine.Fire(Trigger.Die); // works fine
    machine.Fire(Trigger.Die); // fails, calls LogMessage("Unhandled trigger"), but no exceptions.


Dynamic transitions:
    // Occasionally you want to determine at runtime what state to transition
    // to, in that case, specify a callback for the state in the Permit call.
    // In the following example, it is similar to the "Schrodinger's Cat"
    // scenario where the cat dies based on Uranium decay.
    // What will actually happen if Alive is chosen is that it will exit and
    // re-enter the Alive state, which might be a misnomer.

    private enum State { Alive, Dead }
    private enum Trigger { UraniumDecay }
    var machine = StateMachineBuilder.Create<State, Trigger>()
        .Configure(State.Alive)
            .Permit(Trigger.UraniumDecay, t => new Random().Next(2) == 0 ? State.Alive : State.Dead)
            .End()
        .CreateFactory()
        .Generate(State.Alive);

    machine.Fire(Trigger.UraniumDecay); // is the cat dead or alive now?


Conditional transitions:
    // Sometimes you only want to transition in certain cases, which is why
    // the PermitIf method is there for.
    // The following will do the same "Schrodinger's Cat" scenario, but in a
    // different manner. Also, the state won't re-enter the Alive state.
    // You can make both a dynamic and conditional transition if you want to,
    // but the condition will be called first.

    private enum State { Alive, Dead }
    private enum Trigger { UraniumDecay }
    var machine = StateMachineBuilder.Create<State, Trigger>()
        .Configure(State.Alive)
            .PermitIf(Trigger.UraniumDecay, State.Dead, t => new Random().Next(2) == 0)
            .End()
        .CreateFactory()
        .Generate(State.Alive);

    machine.Fire(Trigger.UraniumDecay); // is the cat dead or alive now?


Ignoring:
    // In certain states, you may want to ignore certain triggers, which will
    // make the state machine not change its state and neither exit nor
    // re-enter its current state.

    private enum State { Alive, Dead }
    private enum Trigger { Kill }
    var machine = StateMachineBuilder.Create<State, Trigger>()
        .Configure(State.Alive)
            .Permit(Trigger.Kill, State.Dead)
            .End()
        .Configure(State.Dead)
            .Ignore(Trigger.Kill)
            .End()
        .CreateFactory()
        .Generate(State.Alive);

    machine.Fire(Trigger.Kill); // Dead
    machine.Fire(Trigger.Kill); // no change


Reentry:
    // If you wish to reenter a state, you can use one of two mechanisms:
    // (1) Use Permit and point back to the current state
    // (2) Use PermitReentry
    // The only difference is that if PermitReentry is defined on a superstate
    // and used on a substate, it will reenter the substate rather than go up
    // to the parent.

    private enum State { Sad, Happy }
    private enum Trigger { EatBanana }
    var machine = StateMachineBuilder.Create<State, Trigger>()
        .Configure(State.Sad)
            .Permit(Trigger.EatBanana, State.Happy)
            .End()
        .Configure(State.Happy)
            .PermitReentry(Trigger.EatBanana)
            .End()
        .CreateFactory()
        .Generate(State.Sad);

    machine.Fire(Trigger.EatBanana); // Happy
    machine.Fire(Trigger.EatBanana); // Happy again

Last edited Nov 17, 2010 at 1:49 AM by ckknight, version 3