The .NET component library bbv.Common (open source – Apache License 2.0) provides a powerful hierarchical state machine.
Its features are:
- value type based (enums, ints, …) resulting in single class state machines.
- actions
- on transitions
- entry and exit actions (parametrizable)
- transaction guards
- hierarchical
- different history behaviours to initialize state always to same state or last active state
- fluent definition interface
- synchronous/asynchronous state machine
- passive state machine handles state transitions synchronuously
- active state machine handles state transitions asynchronously on the worker thread of the state machine
- configurable thorough logging simplifies debugging using log4net (can be replaced easily with custom logging)
- state machine report for textual description of state machine
Let’s look at an example.
The following diagram shows the states of an elevator (okay, I wouldn’t ever use this one 😉 )
The above state machine look like this in code:
var elevator = new PassiveStateMachine<States, Events>("Elevator"); elevator.DefineHierarchyOn( States.Healthy, States.OnFloor, HistoryType.Deep, States.OnFloor, States.Moving); elevator.DefineHierarchyOn( States.Moving, States.MovingUp, HistoryType.Shallow, States.MovingUp, States.MovingDown); elevator.DefineHierarchyOn( States.OnFloor, States.DoorClosed, HistoryType.None, States.DoorClosed, States.DoorOpen); elevator.In(States.Healthy) .On(Events.ErrorOccured).Goto(States.Error); elevator.In(States.Error) .On(Events.Reset).Goto(States.Healthy); elevator.In(States.OnFloor) .ExecuteOnEntry(this.AnnounceFloor) .On(Events.CloseDoor).Goto(States.DoorClosed) .On(Events.OpenDoor).Goto(States.DoorOpen) .On(Events.GoUp).Goto(States.MovingUp).OnlyIf(this.CheckOverload) .On(Events.GoUp).Execute(this.AnnounceOverload) .On(Events.GoDown).Goto(States.MovingDown).OnlyIf(this.CheckOverload) .On(Events.GoUp).Execute(this.AnnounceOverload); elevator.In(States.Moving) .On(Events.Stop).Goto(States.OnFloor);
The above state machine uses these actions and guards:
private void AnnounceFloor() { /* announce floor number */ } private void AnnounceOverload(object[] arguments) { /* announce overload */ } private bool CheckOverload(object[] arguments) { return whetherElevatorHasOverload; }
Now, let’s run the elevator by firing some events onto the state machine:
elevator.Initialize(States.OnFloor); // queue some events to be performed when state machine is started. elevator.Fire(Events.ErrorOccured); elevator.Fire(Events.Reset); elevator.Start(); // these events are performed immediately elevator.Fire(Events.OpenDoor); elevator.Fire(Events.CloseDoor); elevator.Fire(Events.GoUp); elevator.Fire(Events.Stop); elevator.Fire(Events.OpenDoor); elevator.Stop();
See here for the full documentation.
Das schaut gut aus.
Ich möchte es mit einem Parser kombinieren, also z.B. ein Textfile parsen und aufgrund der geparsten Tokens die passenden Events feuern.
Wäre das der richtige Weg?
Hättest du einen Link, wie man so einen Parser in C# schreiben könnte?
@Robert
Ich bin nicht sicher ob ich deine Frage richtig verstehe. Wenn du die State Machine brauchen willst um einen Parser zu bauen geht das natürlich.
Schau dir doch mal den CsvParser in Projekt bbv.Common an (http://code.google.com/p/bbvcommon/downloads/detail?name=Source-6.243.zip&can=2&q=#makechanges).
Dort wird zwar nicht die State Machine verwendet (sondern ein doppeltes Switch), aber die Vorgehensweise wäre die selbe.
Zudem hast du mich auf die Idee gebracht, den CsvParser umzubauen auf die State Machine (wäre wesentlich besser zu lesen 🙂 ). Wenn du mir also etwas Zeit gibst, dann kannst du das dann dort abschauen.
Hallo Urs
Wir haben eure Statemachine seit letztem Herbst in einigen Projekten im Einsatz und sind sehr zufrieden damit. Wollte an dieser Stelle einfach mal ein Kompliment machen und positives Feedback geben. Vorallem die einfache Formulierung per Fluent API geniessen wir sehr. Wir konnten einige Flag/if-else-Schlachten übersichtlicher gestalten.
Gruss aus Rapperswil
Ich sehe eine Problem mit der bbv.Common Implementierung von Hierarchischen Statusmaschinen: Für einen State ist hier generell nur ein einiziger intialer Substate konfigurierbar. Das birgt Problem beim Starten der Statusmaschine bzw. beim Übergang aus einem Fehlerzustand heraus. Um beim Beispiel des Fahrstuhls zu bleiben: Man kann die Statusmaschine nur starten (der Startübergang von “No State” zu “Error” oder “Healthy” gehört meiner Meinung nach auch noch ins obige Diagramm), wenn sich der Fahrstuhl “OnFloor” befindet UND die “DoorClosed” ist. Denn “Healthy”->”OnFloor”->”DoorClosed” ist der einzige mögliche initiale Subsubstate. Sollte er sich zwischen den Etagen befinden oder die Türen noch geöffnet sein, läßt sich die Automatik des Fahrstuhls nicht starten. Das gleiche gilt für Fehler. Aus einem Fehler kommt man nur durch einen kompletten Reset raus. Das heißt der Fahrstuhl müßte manuell bis genau auf eine Etage gefahren werden und die Tür muss zu sein. Da hilft auch das HistoryType.Deep für “Healthy” nicht. Man kann nicht davon ausgehen, dass beim Reentry nach einen Fehler der Zustand des Fahrstuhls noch der gleiche ist, wie zu dem Zeitpunk da er in den Fehlerzustand gegangen ist.
Gerade diese Fehler- und Anlauftoleranz ist in der (unserer) Praxis aber ziemlich wichtig und ich versuche gerade zu verstehen, wie ein Design aussehen müßte, das mit sowas klarkommt. Meine Idee war, mehrere Intialie Substates für einen State zuzulassen. Dann muss es nur irgendwo eine Logik geben, welche entscheidet, was der richtige Startstate ist. Irgendwelche Meinungen/Ideen? Vielleicht sehe ich ja auch nur den Wald vor lauter Bäumen nicht…
Hier noch meine Antwort auf das Mail von Robert (mit ähnlichem Inhalt wie sein Post oben):
Hallo Robert
Wir handhaben das “Reset” Problem etwas anderst. Es gibt grundsätzlich
2 Möglichkeiten, wie das mit der jetzigen State Machine geht:
1) Detaillierte Error Zustände
Es gibt nicht nur einen Error State, sondern mehrere. Je nachdem in
welchem Fehlerzustand man ist, wird ein reset anders ausgeführt und
die Zustandsmaschine ist anschliessend in einem klar definierten
Zustand. Also je nachdem ob der Lift auf einem Floor ist oder nicht,
ist man in einem anderen Fehlerzustand.
2) Reset mit Guards
Wenn der Reset von externen Dingen abhängig ist (z.B. wird der Lift
manuell bewegt und es wird über einen Sensor bestimmt wo er sich denn
beim Reset befindet) dann ist es möglich aus dem (einen) Fehlerzustand
bei einem Reset mit Guards zu arbeiten. Jede Guard prüft dabei auf
eine Möglichkeit des Resets. Jenachdem welche Guard erfolgreich
geprüft wird, wird eine andere Transition ausgeführt und so kann pro
Transition der genaue Zustand nach dem Reset definiert werden.
In beiden Fällen, führt die Reset Transition normalerweise nie in
einen Superstate, sondern immer in einen Leave-State (das Liftbeispiel
auf der Webseite ist halt etwas gar einfach 😉
> Apropos Verantwortung. Was hältst du davon, den Status an sich in eine
> vollwertige Klasse zu kapseln
Dies ist gewollt nicht so umgesetzt. Eine Kapselung in Klassen führt
dazu dass wesentlich mehr Code benötigt wird um eine State-Machine
abzubilden (siehe State-Design-Pattern).
Mit der Verwendung von enums, string, ints, … kann die ganze State
Machine in einer Klasse abgehandelt werden (also die Definition und
Actions). Dies ist vorallem sehr praktisch für uns
da wir die State Machine viel auch für UI Logik verwenden (also ganz
simple State Machines mit ~ 4 States).
> Weißt du zufällig wie ich zur Laufzeit den aktuellen
> State inklusive der gesamten aktuellen Hierarchie (also inklusive
> currentState.Parent.Parent usw.) herausfinden kann?
Die State-Machine verhindet bewusst, dass auf den aktuellen State
zugegriffen werden kann. Das Bedürfniss etwas abhängig von einem
aktuellen zustand zu machen, ist ein starker Code Smell. Denn die
State Machine soll sich ja gerade darum kümmern. Darum ist es besser
ein Event an die State Machine zu schicken und diese abhängig vom
aktuellen Zustand etwas machen zu lassen. Ansonsten gibt es ein
“Shared-State” (also Zustand in und ausserhalb der State Machine)
Problem.
Ähnlich ist es mit den Entry und Exit Actions: diese haben bewusst
keine Information über den Zustand aus welchem man kommt oder in
welchen man geht. Dies Actions müssen unabhängig davon sein und sich
nur auf den Zustand auf welchem sie definiert sind beziehen. Alle
anderen Aktionen müssen auf der Transition definiert werden.
Dies haben wir so umgesetzt, da wir schlechte Erfahrungen bezüglich
Verständlichkeit gemacht haben, wenn der aktuelle
Zustand/Source-Destination-State zugreifbar ist.
Falls du mit dieser Antwort nicht glücklich bist, hast du natürlich
immer noch die Möglichkeit, eine eigene State-machine zu schreiben,
welche von der internen State-Machine ableitet (also so wie das die
Passive- und Active-StateMachine tun). Auf der internen State-Machine
ist der aktuelle Zustand bekannt.
Ich hoffe das hilft dir.
Gruess
Urs