Mettre à jour:
Encore une fois merci pour les exemples, ils ont été très utiles et par la suite je ne veux pas dire Leur enlever quoi que ce soit.
Les exemples actuels, dans la mesure où je les comprends bien & les machines à états, ne sont-ils pas la moitié de ce que nous comprenons habituellement par une machine à états?
En ce sens que les exemples changent d’état mais que cela n’est représenté qu’en changeant la valeur d’une variable (et en autorisant des changements de valeur différents selon les états), alors qu’une machine à états devrait aussi changer son comportement, et comportement non ( seulement) dans le sens de permettre différentes modifications de valeur pour une variable en fonction de l'état, mais dans le sens de permettre l'exécution de différentes méthodes pour différents états.
Ou ai-je une idée fausse des machines d'état et de leur utilisation courante?
Meilleures salutations
Question originale:
J'ai trouvé cette discussion sur les machines d'état et les blocs d'itérateur dans c # et des outils pour créer des machines d'état et ce qui ne l'est pas pour C #, j'ai donc trouvé beaucoup de choses abstraites, mais pour un noob, tout cela est un peu déroutant.
Il serait donc intéressant que quelqu'un fournisse un exemple de code source C # qui réalise une machine à états simple avec peut-être 3,4 états, juste pour en avoir le résumé.
Commençons par ce diagramme d'états simple:
On a:
Vous pouvez convertir cela en C # de différentes manières, par exemple en exécutant une instruction switch sur l'état et la commande en cours, ou en recherchant des transitions dans une table de transitions. Pour cette machine à états simple, je préfère une table de transition, très facile à représenter avec une variable Dictionary
:
using System;
using System.Collections.Generic;
namespace Juliet
{
public enum ProcessState
{
Inactive,
Active,
Paused,
Terminated
}
public enum Command
{
Begin,
End,
Pause,
Resume,
Exit
}
public class Process
{
class StateTransition
{
readonly ProcessState CurrentState;
readonly Command Command;
public StateTransition(ProcessState currentState, Command command)
{
CurrentState = currentState;
Command = command;
}
public override int GetHashCode()
{
return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
public override bool Equals(object obj)
{
StateTransition other = obj as StateTransition;
return other != null && this.CurrentState == other.CurrentState && this.Command == other.Command;
}
}
Dictionary<StateTransition, ProcessState> transitions;
public ProcessState CurrentState { get; private set; }
public Process()
{
CurrentState = ProcessState.Inactive;
transitions = new Dictionary<StateTransition, ProcessState>
{
{ new StateTransition(ProcessState.Inactive, Command.Exit), ProcessState.Terminated },
{ new StateTransition(ProcessState.Inactive, Command.Begin), ProcessState.Active },
{ new StateTransition(ProcessState.Active, Command.End), ProcessState.Inactive },
{ new StateTransition(ProcessState.Active, Command.Pause), ProcessState.Paused },
{ new StateTransition(ProcessState.Paused, Command.End), ProcessState.Inactive },
{ new StateTransition(ProcessState.Paused, Command.Resume), ProcessState.Active }
};
}
public ProcessState GetNext(Command command)
{
StateTransition transition = new StateTransition(CurrentState, command);
ProcessState nextState;
if (!transitions.TryGetValue(transition, out nextState))
throw new Exception("Invalid transition: " + CurrentState + " -> " + command);
return nextState;
}
public ProcessState MoveNext(Command command)
{
CurrentState = GetNext(command);
return CurrentState;
}
}
public class Program
{
static void Main(string[] args)
{
Process p = new Process();
Console.WriteLine("Current State = " + p.CurrentState);
Console.WriteLine("Command.Begin: Current State = " + p.MoveNext(Command.Begin));
Console.WriteLine("Command.Pause: Current State = " + p.MoveNext(Command.Pause));
Console.WriteLine("Command.End: Current State = " + p.MoveNext(Command.End));
Console.WriteLine("Command.Exit: Current State = " + p.MoveNext(Command.Exit));
Console.ReadLine();
}
}
}
Par préférence, j'aime concevoir mes machines d'état avec une fonction GetNext
pour renvoyer l'état suivant de manière déterministe , et une fonction MoveNext
pour muter la machine d'état.
Vous pouvez utiliser l'une des machines à états finis open source existantes. Par exemple. bbv.Common.StateMachine disponible à l'adresse http://code.google.com/p/bbvcommon/wiki/StateMachine . Il a une syntaxe fluide très intuitive et de nombreuses fonctionnalités telles que des actions d’entrée/sortie, des actions de transition, des protections, une implémentation hiérarchique et passive (exécutée sur le thread de l’appelant) et une implémentation active les événements sont ajoutés à une file d'attente).
En prenant Juliets par exemple, la définition de la machine à états devient très simple:
var fsm = new PassiveStateMachine<ProcessState, Command>();
fsm.In(ProcessState.Inactive)
.On(Command.Exit).Goto(ProcessState.Terminated).Execute(SomeTransitionAction)
.On(Command.Begin).Goto(ProcessState.Active);
fsm.In(ProcessState.Active)
.ExecuteOnEntry(SomeEntryAction)
.ExecuteOnExit(SomeExitAction)
.On(Command.End).Goto(ProcessState.Inactive)
.On(Command.Pause).Goto(ProcessState.Paused);
fsm.In(ProcessState.Paused)
.On(Command.End).Goto(ProcessState.Inactive).OnlyIf(SomeGuard)
.On(Command.Resume).Goto(ProcessState.Active);
fsm.Initialize(ProcessState.Inactive);
fsm.Start();
fsm.Fire(Command.Begin);
Update: l'emplacement du projet a été déplacé vers: https://github.com/appccelerate/statemachine
Voici un exemple de machine à états finis très classique, modélisant un dispositif électronique très simplifié (comme un téléviseur)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace fsm
{
class Program
{
static void Main(string[] args)
{
var fsm = new FiniteStateMachine();
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.PlugIn);
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.TurnOn);
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.TurnOff);
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.TurnOn);
Console.WriteLine(fsm.State);
fsm.ProcessEvent(FiniteStateMachine.Events.RemovePower);
Console.WriteLine(fsm.State);
Console.ReadKey();
}
class FiniteStateMachine
{
public enum States { Start, Standby, On };
public States State { get; set; }
public enum Events { PlugIn, TurnOn, TurnOff, RemovePower };
private Action[,] fsm;
public FiniteStateMachine()
{
this.fsm = new Action[3, 4] {
//PlugIn, TurnOn, TurnOff, RemovePower
{this.PowerOn, null, null, null}, //start
{null, this.StandbyWhenOff, null, this.PowerOff}, //standby
{null, null, this.StandbyWhenOn, this.PowerOff} }; //on
}
public void ProcessEvent(Events theEvent)
{
this.fsm[(int)this.State, (int)theEvent].Invoke();
}
private void PowerOn() { this.State = States.Standby; }
private void PowerOff() { this.State = States.Start; }
private void StandbyWhenOn() { this.State = States.Standby; }
private void StandbyWhenOff() { this.State = States.On; }
}
}
}
Certains auto-promo sans vergogne ici, mais il y a quelque temps, j'ai créé une bibliothèque appelée YieldMachine qui permet de décrire une machine d'état à complexité limitée de manière très simple et claire. Par exemple, considérons une lampe:
Notez que cette machine à états a 2 déclencheurs et 3 états. Dans le code YieldMachine, nous écrivons une seule méthode pour tous les comportements liés aux états, dans laquelle nous commettons l'atrocité horrible d'utiliser goto
pour chaque état. Un déclencheur devient une propriété ou un champ de type Action
, orné d'un attribut appelé Trigger
. J'ai commenté le code du premier état et ses transitions ci-dessous; les états suivants suivent le même schéma.
public class Lamp : StateMachine
{
// Triggers (or events, or actions, whatever) that our
// state machine understands.
[Trigger]
public readonly Action PressSwitch;
[Trigger]
public readonly Action GotError;
// Actual state machine logic
protected override IEnumerable WalkStates()
{
off:
Console.WriteLine("off.");
yield return null;
if (Trigger == PressSwitch) goto on;
InvalidTrigger();
on:
Console.WriteLine("*shiiine!*");
yield return null;
if (Trigger == GotError) goto error;
if (Trigger == PressSwitch) goto off;
InvalidTrigger();
error:
Console.WriteLine("-err-");
yield return null;
if (Trigger == PressSwitch) goto off;
InvalidTrigger();
}
}
Bref et gentil, hein!
Cette machine à états est contrôlée simplement en lui envoyant des déclencheurs:
var sm = new Lamp();
sm.PressSwitch(); //go on
sm.PressSwitch(); //go off
sm.PressSwitch(); //go on
sm.GotError(); //get error
sm.PressSwitch(); //go off
Juste pour clarifier, j'ai ajouté quelques commentaires au premier état pour vous aider à comprendre comment l'utiliser.
protected override IEnumerable WalkStates()
{
off: // Each goto label is a state
Console.WriteLine("off."); // State entry actions
yield return null; // This means "Wait until a
// trigger is called"
// Ah, we got triggered!
// perform state exit actions
// (none, in this case)
if (Trigger == PressSwitch) goto on; // Transitions go here:
// depending on the trigger
// that was called, go to
// the right state
InvalidTrigger(); // Throw exception on
// invalid trigger
...
Cela fonctionne parce que le compilateur C # a créé une machine à états en interne pour chaque méthode utilisant yield return
. Cette construction est généralement utilisée pour créer paresseusement des séquences de données, mais dans ce cas, nous ne sommes pas réellement intéressés par la séquence renvoyée (qui est de toute façon nulle), mais par le comportement d'état créé sous le capot.
La classe de base StateMachine
effectue une réflexion sur la construction pour attribuer un code à chaque action [Trigger]
, ce qui définit le membre Trigger
et fait avancer la machine à états.
Mais vous n'avez pas vraiment besoin de comprendre les internes pour pouvoir l'utiliser.
Vous pouvez coder un bloc itérateur qui vous permet d'exécuter un bloc de code de manière orchestrée. La manière dont le bloc de code est fragmenté ne doit pas correspondre à quoi que ce soit, c'est simplement comment vous voulez le coder. Par exemple:
IEnumerable<int> CountToTen()
{
System.Console.WriteLine("1");
yield return 0;
System.Console.WriteLine("2");
System.Console.WriteLine("3");
System.Console.WriteLine("4");
yield return 0;
System.Console.WriteLine("5");
System.Console.WriteLine("6");
System.Console.WriteLine("7");
yield return 0;
System.Console.WriteLine("8");
yield return 0;
System.Console.WriteLine("9");
System.Console.WriteLine("10");
}
Dans ce cas, lorsque vous appelez CountToTen, rien ne s'exécute pour le moment. Ce que vous obtenez est en réalité un générateur de machine d'état, pour lequel vous pouvez créer une nouvelle instance de la machine d'état. Vous faites cela en appelant GetEnumerator (). IEnumerator est une machine à états que vous pouvez piloter en appelant MoveNext (...).
Ainsi, dans cet exemple, la première fois que vous appelez MoveNext (...), "1" est écrit sur la console et la prochaine fois que vous appelez MoveNext (...), vous verrez 2, 3, 4 et puis 5, 6, 7, puis 8, puis 9, 10. Comme vous pouvez le constater, il s’agit d’un mécanisme utile pour orchestrer la manière dont les choses devraient se dérouler.
Im post annother answer ici car il s’agit d’une machine à états d’un point de vue différent; très visuel.
Ma réponse originale est un code classique impérial. Je pense que c'est assez visuel car le code va parce que c'est un tableau qui simplifie la visualisation de la machine à états. L'inconvénient est que vous devez écrire tout cela. La réponse de Remos _ soulage l'effort d'écrire le code de la plaque de chaudière mais est beaucoup moins visuelle. Il y a la troisième alternative; vraiment dessiner la machine d'état.
Si vous utilisez .NET et que vous pouvez cibler la version 4 de l'exécution, vous avez la possibilité d'utiliser les activités de la machine à états de workflow. Celles-ci vous permettent essentiellement de dessiner la machine d'état (comme dans le diagramme de Juliet }) et de l'exécuter à l'exécution WF.
Voir l'article MSDN Création de machines d'état avec Windows Workflow Foundation pour plus d'informations, et ce site CodePlex } pour la dernière version.
C’est l’option que je préférerais toujours cibler .NET car il est facile à voir, à modifier et à expliquer aux non-programmeurs; les images valent mille mots comme on dit!
Il est utile de se rappeler que les machines à états sont une abstraction et que vous n'avez pas besoin d'outils particuliers pour en créer une. Cependant, les outils peuvent être utiles.
Vous pouvez par exemple réaliser une machine à états avec les fonctions suivantes:
void Hunt(IList<Gull> gulls)
{
if (gulls.Empty())
return;
var target = gulls.First();
TargetAcquired(target, gulls);
}
void TargetAcquired(Gull target, IList<Gull> gulls)
{
var balloon = new WaterBalloon(weightKg: 20);
this.Cannon.Fire(balloon);
if (balloon.Hit)
{
TargetHit(target, gulls);
}
else
TargetMissed(target, gulls);
}
void TargetHit(Gull target, IList<Gull> gulls)
{
Console.WriteLine("Suck on it {0}!", target.Name);
Hunt(gulls);
}
void TargetMissed(Gull target, IList<Gull> gulls)
{
Console.WriteLine("I'll get ya!");
TargetAcquired(target, gulls);
}
Cette machine chasserait les mouettes et essaierait de les frapper avec des ballons d'eau. Si cela manque, il essaiera de tirer jusqu'à ce qu'il frappe (cela pourrait être fait avec des attentes réalistes;)), sinon, il jubilera dans la console. Il continue à chasser jusqu'à ce qu'il n'y ait plus de mouettes à harceler.
Chaque fonction correspond à chaque état; les états de début et de fin (ou accepter ) ne sont pas affichés. Il y a probablement plus d'états que de modélisés par les fonctions. Par exemple, après avoir tiré le ballon, la machine est vraiment dans un autre état qu'auparavant, mais j'ai décidé que cette distinction était peu pratique.
Une méthode courante consiste à utiliser des classes pour représenter des états, puis les connecter de différentes manières.
Aujourd'hui, je suis profondément ancré dans le modèle de conception de l'état ... J'ai fait et testé ThreadState, qui équivaut (+/-) à Threading en C #, comme décrit dans l'image de Threading en C #
Vous pouvez facilement ajouter de nouveaux états, configurer le passage d’un état à l’autre est très simple car il est encapsulé dans la mise en œuvre de l’état
Implémentation et utilisation dans: Implements .NET ThreadState selon le modèle de conception d'état
J'ai trouvé ce bon tutoriel en ligne et cela m'a aidé à comprendre les machines à états finis.
Le tutoriel est indépendant de la langue, il peut donc être facilement adapté à vos besoins en C #.
En outre, l'exemple utilisé (une fourmi qui cherche de la nourriture) est facile à comprendre.
À partir du tutoriel:
public class FSM {
private var activeState :Function; // points to the currently active state function
public function FSM() {
}
public function setState(state :Function) :void {
activeState = state;
}
public function update() :void {
if (activeState != null) {
activeState();
}
}
}
public class Ant
{
public var position :Vector3D;
public var velocity :Vector3D;
public var brain :FSM;
public function Ant(posX :Number, posY :Number) {
position = new Vector3D(posX, posY);
velocity = new Vector3D( -1, -1);
brain = new FSM();
// Tell the brain to start looking for the leaf.
brain.setState(findLeaf);
}
/**
* The "findLeaf" state.
* It makes the ant move towards the leaf.
*/
public function findLeaf() :void {
// Move the ant towards the leaf.
velocity = new Vector3D(Game.instance.leaf.x - position.x, Game.instance.leaf.y - position.y);
if (distance(Game.instance.leaf, this) <= 10) {
// The ant is extremelly close to the leaf, it's time
// to go home.
brain.setState(goHome);
}
if (distance(Game.mouse, this) <= MOUSE_THREAT_RADIUS) {
// Mouse cursor is threatening us. Let's run away!
// It will make the brain start calling runAway() from
// now on.
brain.setState(runAway);
}
}
/**
* The "goHome" state.
* It makes the ant move towards its home.
*/
public function goHome() :void {
// Move the ant towards home
velocity = new Vector3D(Game.instance.home.x - position.x, Game.instance.home.y - position.y);
if (distance(Game.instance.home, this) <= 10) {
// The ant is home, let's find the leaf again.
brain.setState(findLeaf);
}
}
/**
* The "runAway" state.
* It makes the ant run away from the mouse cursor.
*/
public function runAway() :void {
// Move the ant away from the mouse cursor
velocity = new Vector3D(position.x - Game.mouse.x, position.y - Game.mouse.y);
// Is the mouse cursor still close?
if (distance(Game.mouse, this) > MOUSE_THREAT_RADIUS) {
// No, the mouse cursor has gone away. Let's go back looking for the leaf.
brain.setState(findLeaf);
}
}
public function update():void {
// Update the FSM controlling the "brain". It will invoke the currently
// active state function: findLeaf(), goHome() or runAway().
brain.update();
// Apply the velocity vector to the position, making the ant move.
moveBasedOnVelocity();
}
(...)
}
Je n'ai pas encore essayé d'implémenter un FSM en C #, mais tout cela semble (ou a l'air) très compliqué à la façon dont j'ai géré les FSM dans les langages de bas niveau comme C ou ASM.
Je crois que la méthode que j'ai toujours connue s'appelle quelque chose comme une "boucle itérative". Dans celui-ci, vous avez essentiellement une boucle 'while' qui se termine périodiquement en fonction des événements (interruptions), puis revient à la boucle principale.
Dans les gestionnaires d'interruption, vous transmettriez un CurrentState et renverriez un NextState, qui écrasera ensuite la variable CurrentState dans la boucle principale. Vous faites cela à l'infini jusqu'à la fermeture du programme (ou la réinitialisation du microcontrôleur).
Ce que je vois d’autres réponses semble très compliqué par rapport à la façon dont un FSM est, dans mon esprit, destiné à être mis en œuvre; sa beauté réside dans sa simplicité et le FSM peut être très compliqué avec de très nombreux états et transitions, mais ils permettent à un processus compliqué d'être facilement décomposé et digéré.
Je me rends compte que ma réponse ne devrait pas inclure une autre question, mais je suis obligée de demander: pourquoi ces autres solutions proposées semblent-elles si compliquées?
Ils semblent s'apparenter à frapper un petit clou avec un marteau géant.
Je viens de contribuer ceci:
https://code.google.com/p/ysharp/source/browse/#svn%2Ftrunk%2FStateMachinesPoC
Voici l'un des exemples de démonstration d'envoi direct et indirect de commandes, avec des états tels que IObserver (du signal), répondant ainsi à une source de signal, IObservable (du signal):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Test
{
using Machines;
public static class WatchingTvSampleAdvanced
{
// Enum type for the transition triggers (instead of System.String) :
public enum TvOperation { Plug, SwitchOn, SwitchOff, Unplug, Dispose }
// The state machine class type is also used as the type for its possible states constants :
public class Television : NamedState<Television, TvOperation, DateTime>
{
// Declare all the possible states constants :
public static readonly Television Unplugged = new Television("(Unplugged TV)");
public static readonly Television Off = new Television("(TV Off)");
public static readonly Television On = new Television("(TV On)");
public static readonly Television Disposed = new Television("(Disposed TV)");
// For convenience, enter the default start state when the parameterless constructor executes :
public Television() : this(Television.Unplugged) { }
// To create a state machine instance, with a given start state :
private Television(Television value) : this(null, value) { }
// To create a possible state constant :
private Television(string moniker) : this(moniker, null) { }
private Television(string moniker, Television value)
{
if (moniker == null)
{
// Build the state graph programmatically
// (instead of declaratively via custom attributes) :
Handler<Television, TvOperation, DateTime> stateChangeHandler = StateChange;
Build
(
new[]
{
new { From = Television.Unplugged, When = TvOperation.Plug, Goto = Television.Off, With = stateChangeHandler },
new { From = Television.Unplugged, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler },
new { From = Television.Off, When = TvOperation.SwitchOn, Goto = Television.On, With = stateChangeHandler },
new { From = Television.Off, When = TvOperation.Unplug, Goto = Television.Unplugged, With = stateChangeHandler },
new { From = Television.Off, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler },
new { From = Television.On, When = TvOperation.SwitchOff, Goto = Television.Off, With = stateChangeHandler },
new { From = Television.On, When = TvOperation.Unplug, Goto = Television.Unplugged, With = stateChangeHandler },
new { From = Television.On, When = TvOperation.Dispose, Goto = Television.Disposed, With = stateChangeHandler }
},
false
);
}
else
// Name the state constant :
Moniker = moniker;
Start(value ?? this);
}
// Because the states' value domain is a reference type, disallow the null value for any start state value :
protected override void OnStart(Television value)
{
if (value == null)
throw new ArgumentNullException("value", "cannot be null");
}
// When reaching a final state, unsubscribe from all the signal source(s), if any :
protected override void OnComplete(bool stateComplete)
{
// Holds during all transitions into a final state
// (i.e., stateComplete implies IsFinal) :
System.Diagnostics.Debug.Assert(!stateComplete || IsFinal);
if (stateComplete)
UnsubscribeFromAll();
}
// Executed before and after every state transition :
private void StateChange(IState<Television> state, ExecutionStep step, Television value, TvOperation info, DateTime args)
{
// Holds during all possible transitions defined in the state graph
// (i.e., (step equals ExecutionStep.LeaveState) implies (not state.IsFinal))
System.Diagnostics.Debug.Assert((step != ExecutionStep.LeaveState) || !state.IsFinal);
// Holds in instance (i.e., non-static) transition handlers like this one :
System.Diagnostics.Debug.Assert(this == state);
switch (step)
{
case ExecutionStep.LeaveState:
var timeStamp = ((args != default(DateTime)) ? String.Format("\t\t(@ {0})", args) : String.Empty);
Console.WriteLine();
// 'value' is the state value that we are transitioning TO :
Console.WriteLine("\tLeave :\t{0} -- {1} -> {2}{3}", this, info, value, timeStamp);
break;
case ExecutionStep.EnterState:
// 'value' is the state value that we have transitioned FROM :
Console.WriteLine("\tEnter :\t{0} -- {1} -> {2}", value, info, this);
break;
default:
break;
}
}
public override string ToString() { return (IsConstant ? Moniker : Value.ToString()); }
}
public static void Run()
{
Console.Clear();
// Create a signal source instance (here, a.k.a. "remote control") that implements
// IObservable<TvOperation> and IObservable<KeyValuePair<TvOperation, DateTime>> :
var remote = new SignalSource<TvOperation, DateTime>();
// Create a television state machine instance (automatically set in a default start state),
// and make it subscribe to a compatible signal source, such as the remote control, precisely :
var tv = new Television().Using(remote);
bool done;
// Always holds, assuming the call to Using(...) didn't throw an exception (in case of subscription failure) :
System.Diagnostics.Debug.Assert(tv != null, "There's a bug somewhere: this message should never be displayed!");
// As commonly done, we can trigger a transition directly on the state machine :
tv.MoveNext(TvOperation.Plug, DateTime.Now);
// Alternatively, we can also trigger transitions by emitting from the signal source / remote control
// that the state machine subscribed to / is an observer of :
remote.Emit(TvOperation.SwitchOn, DateTime.Now);
remote.Emit(TvOperation.SwitchOff);
remote.Emit(TvOperation.SwitchOn);
remote.Emit(TvOperation.SwitchOff, DateTime.Now);
done =
(
tv.
MoveNext(TvOperation.Unplug).
MoveNext(TvOperation.Dispose) // MoveNext(...) returns null iff tv.IsFinal == true
== null
);
remote.Emit(TvOperation.Unplug); // Ignored by the state machine thanks to the OnComplete(...) override above
Console.WriteLine();
Console.WriteLine("Is the TV's state '{0}' a final state? {1}", tv.Value, done);
Console.WriteLine();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}
}
Remarque: cet exemple est plutôt artificiel et vise principalement à démontrer un certain nombre de fonctionnalités orthogonales. Il devrait rarement y avoir un réel besoin d'implémenter le domaine de valeur d'état lui-même par une classe complète utilisant le CRTP (voir: http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ) comme ceci.
Voici un cas d'utilisation d'implémentation certainement plus simple et probablement beaucoup plus courant (utilisant un type enum simple en tant que domaine de valeur states), pour la même machine à états et avec le même cas de test:
https://code.google.com/p/ysharp/source/browse/trunk/StateMachinesPoC/WatchingTVSample.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Test
{
using Machines;
public static class WatchingTvSample
{
public enum Status { Unplugged, Off, On, Disposed }
public class DeviceTransitionAttribute : TransitionAttribute
{
public Status From { get; set; }
public string When { get; set; }
public Status Goto { get; set; }
public object With { get; set; }
}
// State<Status> is a shortcut for / derived from State<Status, string>,
// which in turn is a shortcut for / derived from State<Status, string, object> :
public class Device : State<Status>
{
// Executed before and after every state transition :
protected override void OnChange(ExecutionStep step, Status value, string info, object args)
{
if (step == ExecutionStep.EnterState)
{
// 'value' is the state value that we have transitioned FROM :
Console.WriteLine("\t{0} -- {1} -> {2}", value, info, this);
}
}
public override string ToString() { return Value.ToString(); }
}
// Since 'Device' has no state graph of its own, define one for derived 'Television' :
[DeviceTransition(From = Status.Unplugged, When = "Plug", Goto = Status.Off)]
[DeviceTransition(From = Status.Unplugged, When = "Dispose", Goto = Status.Disposed)]
[DeviceTransition(From = Status.Off, When = "Switch On", Goto = Status.On)]
[DeviceTransition(From = Status.Off, When = "Unplug", Goto = Status.Unplugged)]
[DeviceTransition(From = Status.Off, When = "Dispose", Goto = Status.Disposed)]
[DeviceTransition(From = Status.On, When = "Switch Off", Goto = Status.Off)]
[DeviceTransition(From = Status.On, When = "Unplug", Goto = Status.Unplugged)]
[DeviceTransition(From = Status.On, When = "Dispose", Goto = Status.Disposed)]
public class Television : Device { }
public static void Run()
{
Console.Clear();
// Create a television state machine instance, and return it, set in some start state :
var tv = new Television().Start(Status.Unplugged);
bool done;
// Holds iff the chosen start state isn't a final state :
System.Diagnostics.Debug.Assert(tv != null, "The chosen start state is a final state!");
// Trigger some state transitions with no arguments
// ('args' is ignored by this state machine's OnChange(...), anyway) :
done =
(
tv.
MoveNext("Plug").
MoveNext("Switch On").
MoveNext("Switch Off").
MoveNext("Switch On").
MoveNext("Switch Off").
MoveNext("Unplug").
MoveNext("Dispose") // MoveNext(...) returns null iff tv.IsFinal == true
== null
);
Console.WriteLine();
Console.WriteLine("Is the TV's state '{0}' a final state? {1}", tv.Value, done);
Console.WriteLine();
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}
}
'HTH
Quel combat contre StatePattern. Cela répond-il à vos besoins?
Je pense que son contexte est lié, mais ça vaut le coup.
http://en.wikipedia.org/wiki/State_pattern
Cela laisse vos états décider où aller et non la classe "objet".
Bruno
A mon avis, une machine à états n'est pas seulement destinée à changer d'état mais également (très important) à gérer des déclencheurs/événements dans un état spécifique. Si vous voulez mieux comprendre le modèle de conception de la machine d'état, vous trouverez une bonne description dans le livre Modèles de conception Head First, page 321 .
Il ne s'agit pas uniquement des états dans les variables, mais également du traitement des déclencheurs dans les différents états. Grand chapitre (et non, il n’ya aucun frais pour moi en mentionnant ceci :-) qui ne contient qu’une explication facile à comprendre.
J'ai fabriqué cette machine à états générique à partir du code de Juliette. Cela fonctionne génial pour moi.
Ce sont les avantages:
TState
et TCommand
, TransitionResult<TState>
pour avoir plus de contrôle sur les résultats de sortie des méthodes [Try]GetNext()
StateTransition
seulement à travers AddTransition(TState, TCommand, TState)
facilitant son utilisationCode:
public class StateMachine<TState, TCommand>
where TState : struct, IConvertible, IComparable
where TCommand : struct, IConvertible, IComparable
{
protected class StateTransition<TS, TC>
where TS : struct, IConvertible, IComparable
where TC : struct, IConvertible, IComparable
{
readonly TS CurrentState;
readonly TC Command;
public StateTransition(TS currentState, TC command)
{
if (!typeof(TS).IsEnum || !typeof(TC).IsEnum)
{
throw new ArgumentException("TS,TC must be an enumerated type");
}
CurrentState = currentState;
Command = command;
}
public override int GetHashCode()
{
return 17 + 31 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
public override bool Equals(object obj)
{
StateTransition<TS, TC> other = obj as StateTransition<TS, TC>;
return other != null
&& this.CurrentState.CompareTo(other.CurrentState) == 0
&& this.Command.CompareTo(other.Command) == 0;
}
}
private Dictionary<StateTransition<TState, TCommand>, TState> transitions;
public TState CurrentState { get; private set; }
protected StateMachine(TState initialState)
{
if (!typeof(TState).IsEnum || !typeof(TCommand).IsEnum)
{
throw new ArgumentException("TState,TCommand must be an enumerated type");
}
CurrentState = initialState;
transitions = new Dictionary<StateTransition<TState, TCommand>, TState>();
}
/// <summary>
/// Defines a new transition inside this state machine
/// </summary>
/// <param name="start">source state</param>
/// <param name="command">transition condition</param>
/// <param name="end">destination state</param>
protected void AddTransition(TState start, TCommand command, TState end)
{
transitions.Add(new StateTransition<TState, TCommand>(start, command), end);
}
public TransitionResult<TState> TryGetNext(TCommand command)
{
StateTransition<TState, TCommand> transition = new StateTransition<TState, TCommand>(CurrentState, command);
TState nextState;
if (transitions.TryGetValue(transition, out nextState))
return new TransitionResult<TState>(nextState, true);
else
return new TransitionResult<TState>(CurrentState, false);
}
public TransitionResult<TState> MoveNext(TCommand command)
{
var result = TryGetNext(command);
if(result.IsValid)
{
//changes state
CurrentState = result.NewState;
}
return result;
}
}
C'est le type de retour de la méthode TryGetNext:
public struct TransitionResult<TState>
{
public TransitionResult(TState newState, bool isValid)
{
NewState = newState;
IsValid = isValid;
}
public TState NewState;
public bool IsValid;
}
Voici comment créer une OnlineDiscountStateMachine
à partir de la classe générique:
Définissez une énumération OnlineDiscountState
pour ses états et une énumération OnlineDiscountCommand
pour ses commandes.
Définissez une classe OnlineDiscountStateMachine
dérivée de la classe générique à l'aide de ces deux énums
Dérivez le constructeur de base(OnlineDiscountState.InitialState)
de sorte que l'état initial state soit défini sur OnlineDiscountState.InitialState
Utilisez AddTransition
autant de fois que nécessaire
public class OnlineDiscountStateMachine : StateMachine<OnlineDiscountState, OnlineDiscountCommand>
{
public OnlineDiscountStateMachine() : base(OnlineDiscountState.Disconnected)
{
AddTransition(OnlineDiscountState.Disconnected, OnlineDiscountCommand.Connect, OnlineDiscountState.Connected);
AddTransition(OnlineDiscountState.Disconnected, OnlineDiscountCommand.Connect, OnlineDiscountState.Error_AuthenticationError);
AddTransition(OnlineDiscountState.Connected, OnlineDiscountCommand.Submit, OnlineDiscountState.WaitingForResponse);
AddTransition(OnlineDiscountState.WaitingForResponse, OnlineDiscountCommand.DataReceived, OnlineDiscountState.Disconnected);
}
}
utiliser la machine d'état dérivée
odsm = new OnlineDiscountStateMachine();
public void Connect()
{
var result = odsm.TryGetNext(OnlineDiscountCommand.Connect);
//is result valid?
if (!result.IsValid)
//if this happens you need to add transitions to the state machine
//in this case result.NewState is the same as before
Console.WriteLine("cannot navigate from this state using OnlineDiscountCommand.Connect");
//the transition was successfull
//show messages for new states
else if(result.NewState == OnlineDiscountState.Error_AuthenticationError)
Console.WriteLine("invalid user/pass");
else if(result.NewState == OnlineDiscountState.Connected)
Console.WriteLine("Connected");
else
Console.WriteLine("not implemented transition result for " + result.NewState);
}
Il existe 2 packages de machines d'état populaires dans NuGet.
Appccelerate.StateMachine (13,6 Ko de téléchargements + 3,82 Ko de version antérieure (bbv.Common.StateMachine))
StateMachineToolkit (1,56 Ko téléchargements)
La bibliothèque Appccelerate a une bonne documentation , mais elle ne prend pas en charge .NET 4. J'ai donc choisi StateMachineToolkit pour mon projet.
Je recommanderais state.cs . J'ai personnellement utilisé state.js (la version JavaScript) et j'en suis très heureux. Cette version C # fonctionne de manière similaire.
Vous instanciez des états:
// create the state machine
var player = new StateMachine<State>( "player" );
// create some states
var initial = player.CreatePseudoState( "initial", PseudoStateKind.Initial );
var operational = player.CreateCompositeState( "operational" );
...
Vous instanciez des transitions:
var t0 = player.CreateTransition( initial, operational );
player.CreateTransition( history, stopped );
player.CreateTransition<String>( stopped, running, ( state, command ) => command.Equals( "play" ) );
player.CreateTransition<String>( active, stopped, ( state, command ) => command.Equals( "stop" ) );
Vous définissez des actions sur les états et les transitions:
t0.Effect += DisengageHead;
t0.Effect += StopMotor;
Et c'est à peu près tout. Regardez le site pour plus d'informations.
Je pense que la machine à états proposée par Juliet a une erreur: la méthode GetHashCode peut renvoyer le même code de hachage pour deux transitions différentes, par exemple:
Etat = Actif (1), Commande = Pause (2) => Code de hachage = 17 + 31 + 62 = 110
Etat = En pause (2), Commande = Fin (1) => HashCode = 17 + 62 + 31 = 110
Pour éviter cette erreur, la méthode devrait être comme ceci:
public override int GetHashCode()
{
return 17 + 23 * CurrentState.GetHashCode() + 31 * Command.GetHashCode();
}
Alex
FiniteStateMachine est une machine à états simple, écrite en C # Link
Avantages à utiliser ma bibliothèque FiniteStateMachine:
Télécharger DLL Télécharger
Exemple sur LINQPad:
void Main()
{
var machine = new SFM.Machine(new StatePaused());
var output = machine.Command("Input_Start", Command.Start);
Console.WriteLine(Command.Start.ToString() + "-> State: " + machine.Current);
Console.WriteLine(output);
output = machine.Command("Input_Pause", Command.Pause);
Console.WriteLine(Command.Pause.ToString() + "-> State: " + machine.Current);
Console.WriteLine(output);
Console.WriteLine("-------------------------------------------------");
}
public enum Command
{
Start,
Pause,
}
public class StateActive : SFM.State
{
public override void Handle(SFM.IContext context)
{
//Gestione parametri
var input = (String)context.Input;
context.Output = input;
//Gestione Navigazione
if ((Command)context.Command == Command.Pause) context.Next = new StatePaused();
if ((Command)context.Command == Command.Start) context.Next = this;
}
}
public class StatePaused : SFM.State
{
public override void Handle(SFM.IContext context)
{
//Gestione parametri
var input = (String)context.Input;
context.Output = input;
//Gestione Navigazione
if ((Command)context.Command == Command.Start) context.Next = new StateActive();
if ((Command)context.Command == Command.Pause) context.Next = this;
}
}
Autre alternative à ce référentiel https://github.com/lingkodsoft/StateBliss syntaxe couramment utilisée, prend en charge les déclencheurs.
public class BasicTests
{
[Fact]
public void Tests()
{
// Arrange
StateMachineManager.Register(new [] { typeof(BasicTests).Assembly }); //Register at bootstrap of your application, i.e. Startup
var currentState = AuthenticationState.Unauthenticated;
var nextState = AuthenticationState.Authenticated;
var data = new Dictionary<string, object>();
// Act
var changeInfo = StateMachineManager.Trigger(currentState, nextState, data);
// Assert
Assert.True(changeInfo.StateChangedSucceeded);
Assert.Equal("ChangingHandler1", changeInfo.Data["key1"]);
Assert.Equal("ChangingHandler2", changeInfo.Data["key2"]);
}
//this class gets regitered automatically by calling StateMachineManager.Register
public class AuthenticationStateDefinition : StateDefinition<AuthenticationState>
{
public override void Define(IStateFromBuilder<AuthenticationState> builder)
{
builder.From(AuthenticationState.Unauthenticated).To(AuthenticationState.Authenticated)
.Changing(this, a => a.ChangingHandler1)
.Changed(this, a => a.ChangedHandler1);
builder.OnEntering(AuthenticationState.Authenticated, this, a => a.OnEnteringHandler1);
builder.OnEntered(AuthenticationState.Authenticated, this, a => a.OnEnteredHandler1);
builder.OnExiting(AuthenticationState.Unauthenticated, this, a => a.OnExitingHandler1);
builder.OnExited(AuthenticationState.Authenticated, this, a => a.OnExitedHandler1);
builder.OnEditing(AuthenticationState.Authenticated, this, a => a.OnEditingHandler1);
builder.OnEdited(AuthenticationState.Authenticated, this, a => a.OnEditedHandler1);
builder.ThrowExceptionWhenDiscontinued = true;
}
private void ChangingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
var data = changeinfo.DataAs<Dictionary<string, object>>();
data["key1"] = "ChangingHandler1";
}
private void OnEnteringHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
// changeinfo.Continue = false; //this will prevent changing the state
}
private void OnEditedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
{
}
private void OnExitedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
{
}
private void OnEnteredHandler1(StateChangeInfo<AuthenticationState> changeinfo)
{
}
private void OnEditingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
}
private void OnExitingHandler1(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
}
private void ChangedHandler1(StateChangeInfo<AuthenticationState> changeinfo)
{
}
}
public class AnotherAuthenticationStateDefinition : StateDefinition<AuthenticationState>
{
public override void Define(IStateFromBuilder<AuthenticationState> builder)
{
builder.From(AuthenticationState.Unauthenticated).To(AuthenticationState.Authenticated)
.Changing(this, a => a.ChangingHandler2);
}
private void ChangingHandler2(StateChangeGuardInfo<AuthenticationState> changeinfo)
{
var data = changeinfo.DataAs<Dictionary<string, object>>();
data["key2"] = "ChangingHandler2";
}
}
}
public enum AuthenticationState
{
Unauthenticated,
Authenticated
}
}