Hierarchical State Machine with Fluent Definition Syntax (.NET)

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.

About the author

Urs Enzler

5 comments

  • 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

By Urs Enzler

Recent Posts