Created
August 13, 2013 04:07
-
-
Save anonymous/6217808 to your computer and use it in GitHub Desktop.
State machine in C# that I'm toying with. Inspired by https://github.com/soveran/micromachine but for C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| StateMachine car = | |
| StateMachine.Create("off") | |
| .Transition(when: "ignition", off: "park") | |
| .Transition(when: "shift_up", park: "reverse", reverse: "neutral", neutral: "drive") | |
| .Transition(when: "shift_down", drive: "neutral", neutral: "reverse", reverse: "park"); | |
| car.On("drive", () => Console.Log("Hit the road, Jack!")); | |
| car.Any(() => Console.Log("Shifted into " + car.State)); | |
| car.Trigger("ignition"); // P | |
| car.Trigger("shift_up"); // R | |
| car.Trigger("shift_up"); // N | |
| car.Trigger("shift_up"); // D |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Dynamic; | |
| using System.Linq; | |
| namespace StateManagement | |
| { | |
| public class StateMachine : DynamicObject | |
| { | |
| #region Private Fields | |
| private const string SYNTAX = | |
| "The syntax for Transition calls is machine.Transition(when: \"confirm\", pending: \"confirmed\");"; | |
| private readonly Dictionary<object, Dictionary<string, string>> transitionsFor = new Dictionary<object, Dictionary<string, string>>(); | |
| private readonly Dictionary<string, List<Action>> callbacks = new Dictionary<string, List<Action>>(); | |
| #endregion | |
| #region Constructor | |
| public StateMachine(string initial) | |
| { | |
| State = initial; | |
| } | |
| #endregion | |
| #region Public Properties | |
| /// <summary> | |
| /// The current state of the machine. | |
| /// </summary> | |
| public string State { get; private set; } | |
| /// <summary> | |
| /// Returns an array of events registered with this machine. | |
| /// </summary> | |
| public object[] Events | |
| { | |
| get { return transitionsFor.Keys.ToArray(); } | |
| } | |
| /// <summary> | |
| /// Returns an array of the accepted states for this machine. | |
| /// </summary> | |
| public string[] States | |
| { | |
| get { return transitionsFor.SelectMany(x => x.Value, (outer, inner) => inner.Key).Distinct().ToArray(); } | |
| } | |
| #endregion | |
| #region Public Methods | |
| /// <summary> | |
| /// Allows for the dynamic binding of methods. | |
| /// In this instance, the key method is "Transition" | |
| /// which uses the parameter names as the states | |
| /// to transition from and the value of the parameter | |
| /// as the state to transition to. Also allows many | |
| /// transitions to be bound at once. | |
| /// </summary> | |
| /// <example> | |
| /// machine.Transition(when: "start", pending: "ready") | |
| /// </example> | |
| public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) | |
| { | |
| switch (binder.Name) | |
| { | |
| case "Transition": | |
| var index = binder.CallInfo.ArgumentNames.IndexOf("when"); | |
| if (index == -1) | |
| throw new ArgumentException("Missing required argument \"when\". " + SYNTAX); | |
| var trigger = args[index]; | |
| if (binder.CallInfo.ArgumentCount < 2) | |
| throw new ArgumentException("Needs to have at least one transition argument. " + SYNTAX); | |
| if (binder.CallInfo.ArgumentNames.Count != binder.CallInfo.ArgumentCount) | |
| throw new ArgumentException("All arguments must be named. " + SYNTAX); | |
| for (int i = 0; i < args.Length; i++) | |
| { | |
| if (i == index) | |
| continue; | |
| var from = binder.CallInfo.ArgumentNames[i]; | |
| var to = (string) args[i]; | |
| GetEventStates(trigger).Add(from, to); | |
| } | |
| result = this; | |
| return true; | |
| default: | |
| return base.TryInvokeMember(binder, args, out result); | |
| } | |
| } | |
| /// <summary> | |
| /// A non-dynamic version of the Transition method in case you're a hater. | |
| /// </summary> | |
| public StateMachine Transition(string when, string from, string to) | |
| { | |
| GetEventStates(when).Add(from, to); | |
| return this; | |
| } | |
| /// <summary> | |
| /// Affixes a callback to the machine when it enters the given state. | |
| /// </summary> | |
| public StateMachine On(string state, Action callback) | |
| { | |
| GetCallbacks(state).Add(callback); | |
| return this; | |
| } | |
| /// <summary> | |
| /// Affixes a callback to the machine every time it changes state. | |
| /// </summary> | |
| /// <param name="callback"></param> | |
| /// <returns></returns> | |
| public StateMachine Any(Action callback) | |
| { | |
| GetCallbacks("any").Add(callback); | |
| return this; | |
| } | |
| /// <summary> | |
| /// Returns whether triggering the event will cause the machine to change state. | |
| /// </summary> | |
| public bool Check(object trigger) | |
| { | |
| if (!transitionsFor.ContainsKey(trigger)) | |
| throw new InvalidEventException(trigger); | |
| return transitionsFor[trigger].ContainsKey(State) || transitionsFor[trigger].ContainsKey("any"); | |
| } | |
| /// <summary> | |
| /// Triggers an event on the machine, possibly changing its state and invoking callbacks. | |
| /// </summary> | |
| public bool Trigger(object trigger) | |
| { | |
| if (!Check(trigger)) | |
| return false; | |
| State = GetTransition(trigger); | |
| var cb = GetTriggerCallbacks(State); | |
| foreach (var action in cb) | |
| action.Invoke(); | |
| return true; | |
| } | |
| #endregion | |
| #region Static Methods | |
| /// <summary> | |
| /// Just a quick wrapper around the constructor | |
| /// that returns the StateMachine as a dynamic | |
| /// for nice, fluent use by the calling code. | |
| /// </summary> | |
| public static dynamic Create(string initial) | |
| { | |
| return new StateMachine(initial); | |
| } | |
| #endregion | |
| #region Private Methods | |
| private string GetTransition(object e) | |
| { | |
| var t = transitionsFor[e]; | |
| return t.ContainsKey(State) ? t[State] : t["any"]; | |
| } | |
| private IEnumerable<Action> GetTriggerCallbacks(string state) | |
| { | |
| foreach (var callback in GetCallbacks(state)) | |
| yield return callback; | |
| foreach (var callback in GetCallbacks("any")) | |
| yield return callback; | |
| } | |
| private List<Action> GetCallbacks(string state) | |
| { | |
| if (callbacks.ContainsKey(state)) | |
| return callbacks[state]; | |
| var newCallback = new List<Action>(); | |
| callbacks.Add(state, newCallback); | |
| return newCallback; | |
| } | |
| private Dictionary<string, string> GetEventStates(object trigger) | |
| { | |
| if (transitionsFor.ContainsKey(trigger)) | |
| return transitionsFor[trigger]; | |
| var states = new Dictionary<string, string>(); | |
| transitionsFor.Add(trigger, states); | |
| return states; | |
| } | |
| #endregion | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Probably should have been logged in when I uploaded this ;)