Last active
October 6, 2024 07:46
-
-
Save victorjonsson/7c1d6b03da01121859c7155596e287ab to your computer and use it in GitHub Desktop.
Thread safe and generic state machine, that can be made infinite and guards against invalid transisitions
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
namespace TestingStuff | |
{ | |
/// <summary> | |
/// Thread safe and generic state machine, that can be made infinite and guards against invalid state transisitions | |
/// | |
/// public enum MyStates {Init, Running, Stopped} | |
/// var finiteStateMachine = new StateMachine<MyStates>(); | |
/// | |
/// // Infinite state machine that can go back to running or init state once stopped | |
/// var infiniteStateMachine = new StateMachine<MyStates>(new Transition[] { | |
/// new Transition(MyState.Init), | |
/// new Transition(MyState.Running), | |
/// new Transition(MyState.Stopped, new object[] {MyState.Init, MyState.Running}), | |
/// }); | |
/// | |
/// // Infinite state machine, allowing any kind of transitions | |
/// var infiniteStateMachine = new StateMachine<MyState>(allowAnyTransitions: true); | |
/// | |
/// // More complex | |
/// public enum Connection = {NotConnected, Connecting, Connected, Disconnecting} | |
/// var infiniteStateMachine = new StateMachine<MyStates>(new Transition[] { | |
/// new Transition(Connection.NotConnected, new object[] { Connection.Connecting, Connection.Connected }), | |
/// new Transition(Connection.Connecting, new object[] { Connection.NotConnected, Connection.Connected }), | |
/// new Transition(Connection.Connected, new object[] { "*" }), // can turn into any state once connected | |
/// new Transition(Connection.Disconnecting, new object[] { Connection.NotConnected }), | |
/// }); | |
/// | |
/// | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
public class StateMachine<T> where T : Enum | |
{ | |
private readonly Transition<T>[] _transistions; | |
private readonly bool _allTransitionsAllowed = false; | |
private object _lock = new object(); | |
private int _currentStateIndex; | |
public StateMachine(bool allowAnyTransitions, T initialState = default) | |
{ | |
var canTransitionTo = allowAnyTransitions ? new object[] { "*" } : Array.Empty<object>(); | |
_transistions = CreateTransitionsArray(canTransitionTo); | |
_currentStateIndex = FindStateIndex(initialState); | |
_allTransitionsAllowed = allowAnyTransitions; | |
} | |
public StateMachine(T initialState = default) | |
{ | |
_transistions = CreateTransitionsArray(Array.Empty<object>()); | |
_currentStateIndex = FindStateIndex(initialState); | |
} | |
public StateMachine(Transition<T>[] transistions, T initialState = default) | |
{ | |
_transistions = transistions; | |
_currentStateIndex = FindStateIndex(initialState); | |
_allTransitionsAllowed = transistions.All(t => t.ToString() == "*"); | |
} | |
public T State => _transistions[_currentStateIndex].State; | |
/// <summary> | |
/// Transition to new state, will throw InvalidStateTransitionException if not allowed | |
/// </summary> | |
/// <param name="newState"></param> | |
/// <exception cref="InvalidStateTransitionException"></exception> | |
public void TransitionTo(T newState) | |
{ | |
lock(_lock) | |
{ | |
GuardAgainstInvalidStateTransition(newState); | |
_currentStateIndex = FindStateIndex(newState); | |
} | |
} | |
private void GuardAgainstInvalidStateTransition(T newState) | |
{ | |
var current = _transistions[_currentStateIndex]; | |
if (current.State.Equals(newState)) | |
{ | |
throw new InvalidStateTransitionException($"Transitioning to {newState} but object is already in this state"); | |
} | |
if (_allTransitionsAllowed) | |
{ | |
return; | |
} | |
if (current.canTransitionTo == null || current.canTransitionTo.Length == 0) | |
{ | |
// no rules specified, see that next state is newState | |
var next = _transistions.Length > (_currentStateIndex + 1) ? _transistions[_currentStateIndex + 1] : null; | |
if (next == null) | |
{ | |
throw new InvalidStateTransitionException($"A transition attempt is made to '{newState}' after that the finite state machine has reached its final state ('{current}')"); | |
} | |
if (!next.State.Equals(newState)) | |
{ | |
throw new InvalidStateTransitionException(current.State, newState, new string[] { next.State.ToString() }); | |
} | |
} | |
else if (!current.canTransitionTo.Any(allowedNewState => allowedNewState.ToString() == newState.ToString())) | |
{ | |
throw new InvalidStateTransitionException(current.State, newState, current.canTransitionTo.Select(s => s.ToString()).ToArray()); | |
} | |
} | |
private Transition<T>[] CreateTransitionsArray(object[] canTransitionTo) | |
{ | |
return Enum.GetValues(typeof(T)) | |
.Cast<T>() | |
.Select(v => new Transition<T>(v, canTransitionTo)) | |
.ToArray(); | |
} | |
private int FindStateIndex(T state) | |
{ | |
return _transistions | |
.Select((transition, index) => (state: transition.State, index)) | |
.First(tuple => tuple.state.Equals(state)).index; | |
} | |
} | |
public record Transition<T>(T State, object[]? canTransitionTo = null); | |
public class InvalidStateTransitionException: Exception | |
{ | |
public InvalidStateTransitionException(string message) : base(message) { } | |
public InvalidStateTransitionException(object from, object to, object[] allowed) | |
: base($"Invalid state transition from {from} to {to}, only '{string.Join(", ", allowed.Select(o => o.ToString()))}' allowed") { } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment