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