Warum handgeschriebener Zustandsmaschinencode eine schlechte Idee ist
In eingebetteten Softwareprojekten lebt die kritischste und komplexeste Logik meistens in einer Zustandsmaschine. Motorsteuerung, Kommunikationsprotokoll, Benutzeroberflächenfluss, Energieverwaltung — sie alle beantworten dieselbe Frage: „In welchem Zustand befinde ich mich gerade, und wie reagiere ich auf welches Ereignis?"
Und die meisten Teams beantworten diese Frage auf dieselbe Weise: switch/case-Blöcke, enum-Definitionen, Zustandsvariablen, if-Ketten. Es funktioniert. Eine Weile.
Ein vertrautes Szenario
Stellen Sie sich eine Fahrzeugtürsteuerung vor. Die erste Version hat vier Zustände: Offen, Schließend, Geschlossen, Öffnend. Der Code umfasst 200 Zeilen — lesbar, testbar.
Sechs Monate später ändern sich die Anforderungen:
- Automatikbetrieb hinzugefügt — die Tür soll sich nach einer festgelegten Zeit selbst schließen
- Handbetrieb hinzugefügt — der Benutzer kann das automatische Verhalten deaktivieren
- Das Umschalten zwischen den beiden Modi muss zur Laufzeit möglich sein
- Der letzte Zustand muss nach Stromausfällen gespeichert bleiben
- Neuer Sensor hinzugefügt — vollständig geöffnete und vollständig geschlossene Positionen müssen separat erkannt werden
Der Code hat jetzt 800 Zeilen. Jeder switch-Block prüft jeden Zustand. Die Modusvariable isAutomatic wird in jeder Bedingung ausgewertet. Ein neuer Entwickler verbringt eine Woche damit, den Code zu verstehen.
Das ist Zustandsexplosion — das unvermeidliche Ende handgeschriebener Zustandsmaschinen.
Die wahren Kosten
Die Kosten handgeschriebenen Zustandsmaschinencodes werden in der Regel nicht korrekt erfasst, weil sie sich schleichend ansammeln.
Wartungskosten. Wird ein neuer Zustand oder Übergang hinzugefügt, müssen alle switch-Blöcke angefasst werden. Es ist leicht, eine Stelle zu ändern und eine andere zu vergessen. Der Compiler fängt das nicht ab.
Testkosten. Jede Zustand-Ereignis-Kombination muss getestet werden. 10 Zustände × 10 Ereignisse = 100 Testszenarien. Diese von Hand zu schreiben und aktuell zu halten erfordert erheblichen Aufwand. In den meisten Projekten geschieht es schlicht nicht.
Einarbeitungskosten. Wenn ein neuer Entwickler den Code zum ersten Mal betrachtet, gibt es kein Modell — nur Code. Die Entwurfsabsicht muss aus der Implementierung herausgelesen werden. Das gelingt selten vollständig; stattdessen wird die vorhandene Logik umgangen statt sauber erweitert.
Synchronisationskosten. Selbst wenn ein Designdokument existiert, ist dessen Synchronisation mit dem Code ein eigenständiger Arbeitsaufwand. In den meisten Projekten driften beide im Laufe der Zeit auseinander.
Die Wurzel des Problems
Das Problem ist nicht die Verwendung von switch/case. Das Problem lautet: Modell und Implementierung leben getrennt.
Ein Designer zeichnet ein Diagramm. Ein Entwickler übersetzt dieses Diagramm in Code. Zwischen beiden besteht keine automatische Verbindung. Das Diagramm ändert sich, der Code ändert sich — unabhängig voneinander.
Bei hierarchischen Zustandsmaschinen verschärft sich das noch. Verschachtelte Zustände, History-Pseudozustände, parallele Regionen — diese mit C++-switch/case korrekt zu modellieren ist sowohl schwierig als auch fehleranfällig.
Was der modellbasierte Ansatz verändert
UMTSM nähert sich diesem Problem von einer anderen Seite: Modell und Code aus derselben Quelle ableiten.
Der Entwickler definiert die Zustandsmaschine in einer .umt-Datei. Aus dieser Definition wird:
- Das C/C++-Zustandsmaschinengerüst automatisch generiert
- Test-Fixtures und Mocks werden automatisch generiert
- Die CMake-Build-Konfiguration wird automatisch generiert
Wenn das Modell sich ändert, ändert sich der generierte Code. Nutzerimplementierungen (Aktionscode) bleiben erhalten. Entwurf und Implementierung driften nie auseinander.
Im Türsteuerungsbeispiel wurden alle fünf Anforderungsänderungen in der .umt-Datei abgebildet:
persistent deep history -> ManualMode_Open;
state AutomaticMode
{
state Open
{
entry / resetWaitingTime;
do / wait; // ← Timer-Thread
-> Closing; // ← automatischer Übergang bei Abschluss
Manual -> ManualMode:Open; // ← Moduswechsel in einer Zeile
}
}
Diese sieben Zeilen drücken aus:
- Beim Eintreten in den Zustand
Openwird der Timer zurückgesetzt do / waitläuft als Thread und schließt ab, wenn die Zeit abgelaufen ist- Bei Abschluss wird ein Übergang nach
Closingausgelöst - Kommt das Ereignis
Manual, wird ein Übergang nachManualModeausgelöst
Der generierte C++-Code implementiert diese Semantik korrekt und deterministisch — einschließlich Mutex-Schutz, Thread-Verwaltung und History-Wiederherstellung.
„Aber wir haben bereits funktionierenden Code"
Die meisten Projekte gelangen an diesen Punkt und sagen: „Eine Migration lohnt sich nicht." Das ist verständlich — funktionierenden Code anzufassen birgt Risiken.
Aber die richtige Frage lautet: Was sind die Wartungskosten des bestehenden Codes?
Wie lange dauert jede neue Funktion? Wie viele Wochen braucht ein neuer Entwickler zur Einarbeitung? Ist die Testabdeckung wirklich ausreichend? Wie viele Dateien berührt eine einzige Anforderungsänderung?
Die Antworten auf diese Fragen legen in der Regel die wahren Kosten handgeschriebenen Zustandsmaschinencodes ohne Modell offen.
Fazit
Zustandsmaschinencode von Hand zu schreiben ist nicht falsch — aber es skaliert nicht. Je mehr das System wächst, Anforderungen sich ändern und das Team expandiert, desto weiter klafft die Lücke zwischen Modell und Implementierung — sie schließt sich nicht, sie wächst.
Der Wert des modellbasierten Ansatzes liegt darin: Die Entwurfsabsicht lebt im Quellcode, Testbarkeit ist strukturell sichergestellt, und ein neuer Entwickler versteht das System, indem er die .umt-Datei liest.
UMTSM wurde entwickelt, um diesen Ansatz in eingebettete C/C++-Projekte zu tragen. Im nächsten Beitrag untersuchen wir, wie der generierte Code getestet wird und wie die automatisch generierten Test-Fixtures eingesetzt werden.
Bei Fragen oder Projekten rund um UMTSM können Sie gerne Kontakt aufnehmen.
