State Machine ohne State Pattern (in C++) - sinnvoll?

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Hallo zusammen,

ich stehe vor dem Problem, einen Zustandsautomaten zu implementieren. Ich kenne den State Pattern, empfinde ihn in meiner Situation aber eher als Hemmschuh denn als Heilsbringer; evtl. übersehe ich da etwas und daher möchte ich hier mal um Meinungen bitten.

Die Zielsprache wird (altes) C++ sein.

Wichtig für mich ist, dass die möglichen nächsten Übergänge auswertbar sind, d.h. ich muss später in einer GUI nur genau diese anzeigen und mit der zugehörigen Aktion verknüpfen. Über Buttons kann der Benutzer später die Aktionen wählen.

In Python habe ich mal exemplarisch nachgebaut, wie ich mir eine Lösung vorstellen kann:

Code: Alles auswählen

#!/usr/bin/env python

# eigentlich verschiedene Klassen
TRANSITIONS = {
    "Kommen": ["Arbeit beginnen", "Gehen"],
    "Gehen": ["Kommen"],
    "Arbeit beginnen": ["Arbeit beenden"],
    "Arbeit beenden": ["Arbeit beginnen", "Gehen"]
}

class StateMachine:

    def __init__(self, transitions, last_transition):
        self.transitions = transitions
        self.last_transition = last_transition
        
    @property
    def next_transitions(self):
        return self.transitions[self.last_transition]
        
    def execute(self, transition):
        self.last_transition = transition
        # eigentlich transition.do_something()
        print("Doing: '{}'".format(transition))
        
        
def run(machine):
    while True:
        print("\nAuswahl des nächsten Schritts")
        for index, name in enumerate(machine.next_transitions, 1):
            print(index, name)
        next_transition = int(input("Aktion: ")) - 1
        print()
        machine.execute(machine.next_transitions[next_transition])
        

def main():
    run(StateMachine(TRANSITIONS, "Gehen"))
    

if __name__ == "__main__":
    main()
Wie man sieht basiert meine Idee einfach auf einem Mapping, welches jeweils vom letzten Übergang auf die möglichen folgenden verzweigt. Es wird keinerlei Dynamik zur Laufzeit geben, welche nächsten Übergänge möglich sind. Höchstwahrscheinlich wird es auch keine neuen Zustände geben.

Übersehe ich hier etwas, oder ließe sich der Ansatz so nicht auch einfach in C++ umsetzen? Wo läge ein Nachteil bei meinem Ansatz und ein Vorteil beim "Lehrbuch"-Ansatz?
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

Das State-Pattern simuliert den Wechsel der Klasse eines Objekts zur Laufzeit. Unterschiedliche Klassen entsprechen dabei den unterschiedlichen Zuständen, in dem sich das Objekt befinden kann, und deren Methoden definieren das zulässige/gewünschte Verhalten je nach Zustand. Das State-Pattern ist insbesondere dann sinnvoll, wenn das Objekt selbst entscheiden können soll, wann es sich in welchem Zustand befindet und welche Zustandsübergänge wann durchgeführt werden sollen. Wenn diese Punkte in deinem Anwendungsfall nicht wichtig sind, brauchst du das Pattern IMO nicht.

Dein Fall sieht mir eher danach aus, als ginge es um Command-Objekte (wegen execute() und transition.do_something()), die in einem zustandsbehafteten Kontext ausgeführt werden sollen, wobei der Zustand allerdings nur über die Klasse des Command-Objekts definiert wird. Den Kontext könnte man herausziehen, dann bräuchte man keine Klassen mehr, sondern nur noch Funktionen, denen man den Kontext beim Aufruf zusätzlich übergibt. Dann kommt man zu ungefähr sowas:

Code: Alles auswählen

def komm(context):
    ...

def geh(context):
    ...

def beginne_arbeit(context):
    ...

def beende_arbeit(context):
    ...

TRANSITIONS = {
    komm: [beginne_arbeit, geh],
    geh: [komm],
    beginne_arbeit: [beende_arbeit],
    beende_arbeit: [beginne_arbeit, geh]
}

def run(command, transitions, **context):
    while True:
        print("\nAuswahl des nächsten Schritts")
        for index, func in enumerate(transitions[command], 1):
            print(index, func)
        choice = int(input("Aktion: ")) - 1
        print()
        command = transitions[command][choice]
        command(context)

def main():
    run(komm, TRANSITIONS, foo=2, bar=7)
In C++ müsste man halt Funktionszeiger verwenden.
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Ich brauche vermutlich einen Display-Namen der Zustände... mit Klassen könnte ich das bündeln und bräuchte kein zweites Mapping... mal sehen. Aber evtl. reichen Funktionen tatsächlich aus; das muss ich noch mal prüfen. Ansonsten danke ich Dir für die Klarstellung und die Analyse :-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Antworten