Automatische Maschinen gegen Spaghetti-Code


"Ich liebe Spaghetti-Western, ich hasse Spaghetti-Code"

Der „Spaghetti-Code“ ist ein idealer Ausdruck fĂŒr die Beschreibung von Software, die sowohl aus kognitiver als auch aus Ă€sthetischer Sicht ein dampfendes Chaos darstellt. In diesem Artikel werde ich ĂŒber einen Drei-Punkte-Plan zur Zerstörung eines Spaghetti-Codes sprechen:

  • Wir diskutieren, warum der Spaghetti-Code nicht so lecker ist.
  • EinfĂŒhrung eines neuen Blicks auf die tatsĂ€chliche Funktionsweise des Codes.
  • Wir diskutieren die Frame Machine Notation (FMN) , mit der Entwickler einen Pastenball entwirren können.

Wir alle wissen, wie schwierig es ist, den Code eines anderen zu lesen. Dies kann daran liegen, dass die Aufgabe selbst schwierig ist oder dass die Struktur des Codes zu ... "kreativ" ist. Oft gehen diese beiden Probleme Hand in Hand.

Herausforderungen sind schwierige Aufgaben, und normalerweise kann nichts als eine revolutionĂ€re Entdeckung sie vereinfachen. Es kommt jedoch vor, dass die Softwarestruktur selbst unnötige KomplexitĂ€t hinzufĂŒgt, und dieses Problem ist es wert, gelöst zu werden.

Die HÀsslichkeit des Spaghetti-Codes liegt in seiner komplexen bedingten Logik. Und obwohl das Leben ohne die vielen kniffligen Wenn-Dann-Sonst-Konstrukte schwer vorstellbar ist, zeigt Ihnen dieser Artikel eine bessere Lösung.


Um die Situation mit Spaghetti-Code zu veranschaulichen, mĂŒssen wir zuerst Folgendes drehen:


Knusprige Pasta

In diesem:


Al dente!

Lass uns anfangen zu kochen.

Impliziter Zustand


Um Pasta zu machen, brauchen wir definitiv Wasser zum Kochen. Selbst ein scheinbar einfaches Element mit Spaghetti-Code kann jedoch sehr verwirrend sein.

Hier ist ein einfaches Beispiel:

(temp < 32) 

Was macht diese PrĂŒfung wirklich? NatĂŒrlich teilt es die Zahlenlinie in zwei Teile, aber was bedeuten diese Teile? Ich denke, Sie können eine logische Annahme treffen, aber das Problem ist, dass der Code dies nicht explizit kommuniziert.

Wenn ich wirklich bestĂ€tige, dass sie prĂŒft, ob das Wasser FEST ist [ca. Spur: Laut der Fahrenheit-Skala gefriert das Wasser bei +32 Grad . Was bedeutet logischerweise, dass die RĂŒckgabe falsch ist?

 if (temp < 32) { // SOLID water } else { // not SOLID water. is (LIQUID | GAS) } 

Obwohl die PrĂŒfung die Zahlen in zwei Gruppen unterteilt hat, gibt es tatsĂ€chlich drei logische ZustĂ€nde - fest, flĂŒssig und gasförmig (FEST, FLÜSSIG, GAS)!

Das heißt, diese Zahlenreihe:


Aufteilung nach BedingungsprĂŒfung wie folgt:

 if (temp < 32) { 


 } else { 


 } 

Beachten Sie, was passiert ist, da dies fĂŒr das VerstĂ€ndnis der Art des Spaghetti-Codes sehr wichtig ist. Eine boolesche PrĂŒfung teilte den Zahlenraum in zwei Teile, kategorisierte das System jedoch NICHT als echte logische Struktur aus (SOLID, LIQUID, GAS). Stattdessen teilte die PrĂŒfung den Raum in (SOLID, alles andere).

Hier ist eine Ă€hnliche PrĂŒfung:

 if (temp > 212) { // GAS water } else { // not GAS water. is (SOLID | LIQUID) } 

Optisch sieht es so aus:

 if (temp > 212) { 


 } else { 


 } 

Beachten Sie Folgendes:

  1. Der vollstĂ€ndige Satz möglicher ZustĂ€nde wird nirgendwo angekĂŒndigt
  2. Nirgendwo in bedingten Konstrukten werden ĂŒberprĂŒfbare logische ZustĂ€nde oder Gruppen von ZustĂ€nden deklariert
  3. Einige ZustÀnde werden indirekt nach der Struktur der bedingten Logik und der Verzweigung gruppiert

Ein solcher Code ist fragil, aber sehr verbreitet und nicht so groß, dass er Probleme mit seiner UnterstĂŒtzung verursacht. Machen wir die Situation noch schlimmer.


Ich habe deinen Code sowieso nie gemocht

Der oben gezeigte Code impliziert die Existenz von drei MateriezustĂ€nden - FEST, FLÜSSIG, GAS. Wissenschaftlichen Daten zufolge gibt es jedoch tatsĂ€chlich vier beobachtbare ZustĂ€nde, in denen Plasma (PLASMA) enthalten ist (tatsĂ€chlich gibt es viele andere, aber dies wird fĂŒr uns ausreichen). Obwohl niemand eine Paste aus Plasma herstellt, mĂŒssen wir diesen Zustand auch beibehalten, wenn dieser Code auf Github veröffentlicht wird und dann von einem Doktoranden, der Hochenergiephysik studiert, gegabelt wird.

Wenn jedoch Plasma hinzugefĂŒgt wird, fĂŒhrt der oben gezeigte Code naiv Folgendes aus:

 if (temp < 32) { // SOLID water } else { // not SOLID water. is (LIQUID | GAS) + (PLASMA?) // how did PLASMA get in here?? } if (temp > 212) { // GAS water + (PLASMA) // again with the PLASMA!! } else { // not GAS water. is (SOLID | LIQUID) } 

Es ist wahrscheinlich, dass der alte Code, wenn er zu vielen PlasmazustĂ€nden hinzugefĂŒgt wird, in den Zweigen else bricht. Leider hilft nichts in der Codestruktur, das Vorhandensein eines neuen Zustands zu melden oder Änderungen zu beeinflussen. DarĂŒber hinaus sind Fehler wahrscheinlich unauffĂ€llig, dh es ist am schwierigsten, sie zu finden. Sag einfach nein zu den Insekten in den Spaghetti.

Kurz gesagt, das Problem ist folgendes: Boolesche PrĂŒfungen werden verwendet, um ZustĂ€nde indirekt zu bestimmen. Logische ZustĂ€nde werden oft nicht deklariert und sind im Code nicht sichtbar. Wie wir oben gesehen haben, kann vorhandener Code beschĂ€digt werden, wenn das System neue logische ZustĂ€nde hinzufĂŒgt. Um dies zu vermeiden, sollten Entwickler jede einzelne bedingte PrĂŒfung und Verzweigung erneut ĂŒberprĂŒfen, um sicherzustellen, dass die Codepfade fĂŒr alle ihre logischen ZustĂ€nde weiterhin gĂŒltig sind! Dies ist der Hauptgrund fĂŒr die Verschlechterung großer Codefragmente, wenn diese komplexer werden.

Obwohl es keine Möglichkeiten gibt, bedingte DatenprĂŒfungen vollstĂ€ndig zu beseitigen, verringert jede Technik, die sie minimiert, die CodekomplexitĂ€t.

Schauen wir uns nun eine typische objektorientierte Implementierung einer Klasse an, die ein sehr einfaches Modell des Wasservolumens erstellt. Die Klasse wird Änderungen im Zustand der Wassersubstanz verwalten. Nachdem wir die Probleme der klassischen Lösung dieses Problems untersucht haben, diskutieren wir eine neue Notation namens Frame und zeigen, wie sie mit den entdeckten Schwierigkeiten umgehen kann.

Zuerst das Wasser zum Kochen bringen ...


Die Wissenschaft gab allen möglichen ÜbergĂ€ngen Namen, die eine Substanz machen kann, wenn sich die Temperatur Ă€ndert.


Unsere Klasse ist sehr einfach (und nicht besonders nĂŒtzlich). Es beantwortet die Herausforderungen beim DurchfĂŒhren von ÜbergĂ€ngen zwischen ZustĂ€nden und Ă€ndert die Temperatur, bis sie fĂŒr den gewĂŒnschten Zielzustand geeignet ist:

(Hinweis: Ich habe diesen Pseudocode geschrieben. Verwenden Sie ihn in Ihrer Arbeit nur auf eigene Gefahr und Gefahr.)

 class WaterSample { temp:int Water(temp:int) { this.temp = temp } // gas -> solid func depose() { // If not in GAS state, throw an error if (temp < WATER_GAS_TEMP) throw new IllegalStateError() // do depose while (temp > WATER_SOLID_TEMP) decreaseTemp(1) } // gas -> liquid func condense() { // If not in GAS state, throw an error if (temp < WATER_GAS_TEMP) throw new IllegalStateError() // do condense while (temp > WATER_GAS_TEMP) decreaseTemp(1) } // liquid -> gas func vaporize() { // If not in LIQUID state, throw an error if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() // do vaporize while (temp < WATER_GAS_TEMP) increaseTemp(1) } // liquid -> solid func freeze() { // If not in LIQUID state, throw an error if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() // do freeze while (temp > WATER_SOLID_TEMP) decreaseTemp(1) } // solid -> liquid func melt() { // If not in SOLID state, throw an error if (temp > WATER_SOLID_TEMP) throw new IllegalStateError() // do melt while (temp < WATER_SOLID_TEMP) increaseTemp(1) } // solid -> gas func sublimate() { // If not in SOLID state, throw an error if (temp > WATER_SOLID_TEMP) throw new IllegalStateError() // do sublimate while (temp < WATER_GAS_TEMP) increaseTemp(1) } func getState():string { if (temp < WATER_SOLID_TEMP) return "SOLID" if (temp > WATER_GAS_TEMP) return "GAS" return "LIQUID" } } 

Im Vergleich zum ersten Beispiel weist dieser Code bestimmte Verbesserungen auf. ZunĂ€chst werden die fest codierten „magischen“ Zahlen (32, 212) durch die Konstanten der Zustandstemperaturgrenzen (WATER_SOLID_TEMP, WATER_GAS_TEMP) ersetzt. Diese Änderung beginnt, Staaten expliziter zu machen, wenn auch indirekt.

In diesem Code werden auch ÜberprĂŒfungen auf "defensive Programmierung" angezeigt, die den Methodenaufruf einschrĂ€nken, wenn er sich fĂŒr die Operation in einem ungeeigneten Zustand befindet. Zum Beispiel kann Wasser nicht gefrieren, wenn es keine FlĂŒssigkeit ist - dies verstĂ¶ĂŸt gegen das Naturgesetz. Das HinzufĂŒgen von Watchdog-Bedingungen erschwert jedoch das VerstĂ€ndnis des Zwecks des Codes. Zum Beispiel:

 // liquid -> solid if (!(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP)) throw new IllegalStateError() 

Diese bedingte PrĂŒfung fĂŒhrt Folgendes aus:

  1. ÜberprĂŒft, ob die Temperatur unter der GAS-Grenztemperatur liegt
  2. ÜberprĂŒft, ob die Temperatur die SOLID-Grenztemperatur ĂŒberschreitet
  3. Gibt einen Fehler zurĂŒck, wenn eine dieser PrĂŒfungen nicht wahr ist

Diese Logik ist verwirrend. Erstens wird der flĂŒssige Zustand dadurch bestimmt, was die Substanz nicht ist - ein Feststoff oder ein Gas.

 (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // is liquid? 

Zweitens prĂŒft der Code, ob das Wasser flĂŒssig ist, um festzustellen, ob ein Fehler zurĂŒckgegeben werden muss.

 !(temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) // Seriously? 

Das erste Mal, diese doppelte Negation von ZustĂ€nden zu verstehen, ist nicht einfach. Hier ist eine Vereinfachung, die die KomplexitĂ€t des Ausdrucks geringfĂŒgig verringert:

 bool isLiquidWater = (temp < WATER_GAS_TEMP && temp > WATER_SOLID_TEMP) if (!isLiquidWater) throw new IllegalStateError() 

Dieser Code ist leichter zu verstehen, da der Status isLiquidWater explizit ist .

Jetzt untersuchen wir Techniken, die einen expliziten Zustand festlegen, um Probleme am besten zu lösen. Bei diesem Ansatz werden die logischen ZustÀnde des Systems zur physischen Struktur der Software, was den Code verbessert und sein VerstÀndnis vereinfacht.

Frame Machine Notation


Frame Machine Notation (FMN) ist eine domĂ€nenspezifische Sprache (DSL), die einen kategorialen, methodischen und einfachen Ansatz zum Definieren und Implementieren verschiedener Maschinentypen definiert . Der Einfachheit halber werde ich Frame-Automaten einfach "Maschinen" nennen, da diese Notation theoretische Kriterien fĂŒr verschiedene Typen definieren kann (Zustandsmaschinen, GeschĂ€ftsautomaten und die Top-Entwicklung von Automaten - Turing-Maschinen). Um mehr ĂŒber die verschiedenen Maschinentypen und ihre Anwendung zu erfahren, empfehle ich, die Seite auf Wikipedia zu studieren.

Obwohl die Automatentheorie interessant sein mag (eine SEHR zweifelhafte Aussage), konzentrieren wir uns in diesem Artikel auf die praktische Anwendung dieser leistungsstarken Konzepte zum Erstellen von Systemen und zum Schreiben von Code.

Um dieses Problem zu lösen, fĂŒhrt Frame eine standardisierte Notation ein, die auf drei integrierten Ebenen funktioniert:

  1. Text-DSL zum Definieren von Frame-Controllern mit eleganter und prÀziser Syntax
  2. Eine Reihe von Referenzcodierungsmustern zum Implementieren objektorientierter Klassen in Form von Maschinen, die Frame als "Controller" bezeichnet.
  3. Visuelle Notation, in der FMN verwendet wird, um komplexe Operationen auszudrĂŒcken, die schwer grafisch darzustellen sind - Frame Visual Notation (FVN)

In diesem Artikel werde ich die ersten beiden Punkte betrachten: FMN und Referenzmuster, und ich werde die Diskussion ĂŒber FVN fĂŒr zukĂŒnftige Artikel verlassen.

Frame ist eine Notation, die mehrere wichtige Aspekte hat:

  1. FMN verfĂŒgt ĂŒber Objekte der ersten Ebene im Zusammenhang mit dem Konzept von Automaten, die in objektorientierten Sprachen nicht verfĂŒgbar sind.
  2. Die FMN-Spezifikation definiert Standardimplementierungsmuster im Pseudocode, die zeigen, wie die FMN-Notation implementiert werden kann.
  3. FMN wird in KĂŒrze in jeder objektorientierten Sprache kompilieren (in Arbeit) sein können

Hinweis: Die Referenzimplementierung wird verwendet, um die absolute Äquivalenz der FMN-Notation zu demonstrieren und eine einfache Möglichkeit, sie in einer objektorientierten Sprache zu implementieren. Sie können eine beliebige Methode auswĂ€hlen.

Jetzt werde ich Ihnen die beiden wichtigsten Objekte der ersten Ebene in Frame vorstellen - Frame Events und Frame Controller .

Rahmenereignisse


FrameEvents sind ein wesentlicher Bestandteil der Einfachheit der FMN-Notation. Ein FrameEvent wird als Struktur oder Klasse implementiert, die mindestens die folgenden Elementvariablen enthÀlt:

  • Nachrichten-ID
  • Wörterbuch oder Parameterliste
  • Objekt zurĂŒckgeben

Hier ist der Pseudocode der FrameEvent-Klasse:

 class FrameEvent { var _msg:String var _params:Object var _return:Object FrameEvent(msg:String, params:Object = null) { _msg = msg _params = params } } 

Die Frame-Notation verwendet das @ -Symbol, das das FrameEvent-Objekt identifiziert. Jedes der erforderlichen FrameEvent-Attribute verfĂŒgt ĂŒber ein spezielles Token, um darauf zuzugreifen:

 @|message| :  -    _msg @[param1] :  []      @^ :              _return 

Oft mĂŒssen wir nicht angeben, mit was FrameEvent funktioniert. Da die meisten Kontexte jeweils nur mit einem FrameEvent arbeiten, kann die Notation definitiv vereinfacht werden, sodass nur Attributselektoren verwendet werden. Daher können wir den Zugriff vereinfachen:

 |buttonClick| // Select for a "buttonClick" event _msg [firstName] = "Mark" // Set firstName _params property to "Mark" ^ = "YES" // Set the _return object to "YES" 

Eine solche Notation mag zunĂ€chst seltsam erscheinen, aber bald werden wir sehen, wie eine so einfache Syntax fĂŒr Ereignisse das VerstĂ€ndnis des FMN-Codes erheblich vereinfacht.

Frame Controller


Ein Frame Controller ist eine objektorientierte Klasse, die genau definiert angeordnet ist, um eine Frame-Maschine zu implementieren. Controller-Typen werden durch das PrÀfix # gekennzeichnet :

 #MyController 

Dies entspricht dem folgenden objektorientierten Pseudocode:

 class MyController {} 

Offensichtlich ist diese Klasse nicht besonders nĂŒtzlich. Damit er etwas tun kann, benötigt der Controller mindestens einen Status, um auf Ereignisse zu reagieren.

Die Steuerungen sind so strukturiert, dass sie Blöcke verschiedener Typen enthalten, die durch einen Bindestrich um den Namen des Blocktyps gekennzeichnet sind:

 #MyController<br> -block 1- -block 2- -block 3- 

Ein Controller kann nicht mehr als eine Instanz jedes Blocks haben, und Blocktypen können nur bestimmte Arten von Unterkomponenten enthalten. In diesem Artikel untersuchen wir nur den -machine- Block, der nur ZustÀnde enthalten kann. ZustÀnde werden durch das $ -PrÀfix-Token identifiziert.

Hier sehen wir die FMN fĂŒr eine Steuerung, die eine Maschine mit nur einem Zustand enthĂ€lt:

 #MyController // controller declaration -machine- // machine block $S1 // state declaration 

Hier ist die Implementierung des obigen FMN-Codes:

 class MyController { // -machine- var _state(e:FrameEvent) = S1 // initialize state variable // to $S1 func S1(e:FrameEvent) { // state $S1 does nothing } } 

Die Implementierung des Maschinenblocks besteht aus folgenden Elementen:

  1. _state Variable, die sich auf eine Funktion des aktuellen Status bezieht. Es wird mit der ersten Zustandsfunktion in der Steuerung initialisiert.
  2. eine oder mehrere Zustandsmethoden

Die Frame-Statusmethode wird als Funktion mit der folgenden Signatur definiert:

 func MyState(e:FrameEvent); 

Nachdem Sie diese Grundlagen fĂŒr die Implementierung des Maschinenblocks definiert haben, können Sie sehen, wie gut das FrameEvent mit der Maschine interagiert.

Schnittstelleneinheit


Das Zusammenspiel von FrameEvents, die den Betrieb der Maschine steuern, ist das Wesentliche fĂŒr die Einfachheit und LeistungsfĂ€higkeit der Frame-Notation. Wir haben die Frage, woher FrameEvents kommen, jedoch noch nicht beantwortet - wie gelangen sie in den Controller, um ihn zu steuern? Eine Option: Externe Clients können selbst FrameEvents erstellen und initialisieren und dann direkt die Methode aufrufen, auf die die _state-Membervariable verweist:

 myController._state(new FrameEvent("buttonClick")) 

Eine viel bessere Alternative wĂ€re, eine gemeinsame Schnittstelle zu erstellen, die einen direkten Aufruf der Mitgliedsvariablen _state umschließt:

 myController.sendEvent(new FrameEvent("buttonClick")) 

Der problemloseste Weg, der der ĂŒblichen Art der Erstellung objektorientierter Software entspricht, besteht darin, allgemeine Methoden zu erstellen, die ein Ereignis im Namen des Clients an den internen Computer senden:

 class MyController { func buttonClick() { FrameEvent e = new FrameEvent("buttonClick") _state(e) return e._return } } 

Frame definiert die Syntax fĂŒr einen Schnittstellenblock , der Methoden enthĂ€lt, die Aufrufe in eine gemeinsame Schnittstelle fĂŒr FrameEvents verwandeln.

 #MyController -interface- buttonClick ... 

Der interface hat viele andere Funktionen, aber dieses Beispiel gibt uns eine allgemeine Vorstellung davon, wie dies funktioniert. Ich werde in den folgenden Artikeln der Reihe weitere ErklÀrungen geben.

Lassen Sie uns nun die Funktionsweise des Frame-Automaten weiter untersuchen.

Ereignishandler


Obwohl wir gezeigt haben, wie man ein Auto definiert, haben wir noch keine Notation, mit der wir etwas tun können. Um Ereignisse zu verarbeiten, mĂŒssen wir 1) das zu verarbeitende Ereignis auswĂ€hlen können und 2) es an das durchgefĂŒhrte Verhalten anhĂ€ngen.

Hier ist ein einfacher Frame-Controller, der die Infrastruktur fĂŒr die Behandlung von Ereignissen bereitstellt:

 #MyController // controller declaration -machine- // machine block $S1 // state declaration |e1| ^ // e1 event handler and return 

Wie oben angegeben, verwendet die FMN-Notation fĂŒr den Zugriff auf das _msg Attribut des _msg Ereignisses Klammern aus vertikalen Linien:

 |messageName| 

FMN verwendet auch ein Exponententoken, das die return-Anweisung darstellt. Die oben gezeigte Steuerung wird wie folgt implementiert:

 class MyController { // #MyController // -machine- var _state(e:FrameEvent) = S1 func S1(e:FrameEvent) { // $S1 if (e._msg == "e1") { // |e1| return // ^ } } } 

Hier sehen wir, wie deutlich die FMN-Notation einem Implementierungsmuster entspricht, das leicht zu verstehen und zu codieren ist.

Nachdem wir diese grundlegenden Aspekte von Ereignissen, Steuerungen, Maschinen, ZustÀnden und Ereignishandlern festgelegt haben, können wir mit ihrer Hilfe echte Probleme lösen.

Single-Focus-Maschinen


Oben haben wir uns einen zustandslosen Controller angesehen, der ziemlich nutzlos war.

 #MyController 

Ein Schritt höher in der Nahrungskette ist eine Klasse mit einem einzigen Zustand, der zwar nicht nutzlos, aber einfach langweilig ist. Aber zumindest macht er wenigstens etwas .

Lassen Sie uns zunÀchst sehen, wie eine Klasse mit nur einem (implizierten) Status implementiert wird:

 class Mono { String status() { return "OFF" } } 

Hier wird kein Status deklariert oder sogar impliziert, aber nehmen wir an, dass sich das System im Status "Working" befindet, wenn der Code etwas tut.

Wir werden auch eine wichtige Idee vorstellen: Schnittstellenaufrufe werden als Ă€hnlich wie das Senden eines Ereignisses an ein Objekt betrachtet. Daher kann der obige Code als ein Verfahren zum Übertragen des | Status | betrachtet werden die Mono-Klasse, immer im Zustand $ Working.

Diese Situation kann mithilfe der Ereignisbindungstabelle visualisiert werden:


Schauen wir uns nun FMN an, das dieselbe FunktionalitĂ€t demonstriert und mit derselben Bindungstabelle ĂŒbereinstimmt:

 #Mono -machine- $Working |status| ^("OFF") 

So sieht die Implementierung aus:

 class Mono { // #Mono // -machine- var _state(e:FrameEvent) = Working // initialize start state func Working(e:FrameEvent) { // $Working if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } } 

Sie können feststellen, dass wir auch eine neue Notation fĂŒr die return-Anweisung eingefĂŒhrt haben. Dies bedeutet, dass der Ausdruck ausgewertet und das Ergebnis an die Schnittstelle zurĂŒckgegeben wird:

 ^(return_expr) 

Dieser Operator ist Àquivalent

 @^ = return_expr 

oder einfach

 ^ = return_expr 

Alle diese Operatoren sind funktional Àquivalent und Sie können jeden von ihnen verwenden, aber ^(return_expr) sieht am ausdrucksstÀrksten aus.

Schalten Sie den Herd ein


Bisher haben wir einen Controller mit 0 ZustĂ€nden und einen Controller mit 1 Zustand gesehen. Sie sind noch nicht sehr nĂŒtzlich, aber wir stehen bereits vor etwas Interessantem.

Um unsere Nudeln zu kochen, mĂŒssen Sie zuerst den Herd einschalten. Das Folgende ist eine einfache Switch-Klasse mit einer einzelnen booleschen Variablen:

 class Switch { boolean _isOn; func status() { if (_isOn) { return "ON"; } else { return "OFF"; } } } 

Obwohl dies auf den ersten Blick nicht offensichtlich ist, implementiert der oben gezeigte Code die folgende Tabelle der Ereignisbindungen:


Zum Vergleich hier ein FMN fĂŒr das gleiche Verhalten:

 #Switch1 -machine- $Off |status| ^("OFF") $On |status| ^("ON") 

Jetzt sehen wir, wie genau die Frame-Notation dem Zweck unseres Codes entspricht - das AnhĂ€ngen eines Ereignisses (Methodenaufrufs) an das Verhalten basierend auf dem Status, in dem sich der Controller befindet. DarĂŒber hinaus entspricht die Implementierungsstruktur auch der Bindungstabelle:

 class Switch1 { // #Switch1 // -machine- var _state(e:FrameEvent) = Off func Off(e:FrameEvent) { // $Off if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } func On(e:FrameEvent) { // $On if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

In der Tabelle können Sie den Zweck des Controllers in seinen verschiedenen ZustÀnden schnell verstehen. Sowohl die Frame-Notationsstruktur als auch das Implementierungsmuster haben Àhnliche Vorteile.

Unser Switch weist jedoch ein spĂŒrbares Funktionsproblem auf. Es wird im Status $ Off initialisiert, kann aber nicht in den Status $ On wechseln! Dazu mĂŒssen wir einen ZustandsĂ€nderungsoperator eingeben.

Status Àndern


Die Anweisung zur StatusÀnderung lautet wie folgt:

 ->> $NewState 

Jetzt können wir diesen Operator verwenden, um zwischen $ Off und $ On zu wechseln:

 #Switch2 -machine- $Off |toggle| ->> $On ^ |status| ^("OFF") $On |toggle| ->> $Off ^ |status| ^("ON") 

Und hier ist die entsprechende Ereignisbindungstabelle:


Neues Ereignis | umschalten | Löst jetzt eine Änderung aus, die einfach die beiden ZustĂ€nde durchlĂ€uft. Wie kann eine ZustandsĂ€nderungsoperation implementiert werden?

Nirgendwo ist es einfacher. Hier ist die Implementierung von Switch2:

 class Switch2 { // #Switch2 // -machine- var _state(e:FrameEvent) = Off func Off(e:FrameEvent) { if (e._msg == "toggle") { // |toggle| _state = On // ->> $On return // ^ } if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } func On(e:FrameEvent) { if (e._msg == "toggle") { // |toggle| _state = Off // ->> $Off return // ^("OFF") } if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

Sie können auch die letzte Verbesserung in Switch2 vornehmen, sodass Sie nicht nur zwischen Status wechseln können, sondern auch den Status explizit festlegen:

 #Switch3 -machine- $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON") 

Im Gegensatz zum | toggle | -Ereignis, wenn | turnOn | Wird gesendet, wenn Switch3 bereits eingeschaltet ist, oder | turnOff |, wenn es bereits ausgeschaltet ist, wird die Nachricht ignoriert und es passiert nichts. Diese kleine Verbesserung gibt dem Client die Möglichkeit, explizit anzugeben, in welchem ​​Zustand sich der Switch befinden soll:

 class Switch3 { // #Switch3 // -machine- var _state(e:FrameEvent) = Off /********************************** $Off |turnOn| ->> $On ^ |toggle| ->> $On ^ |status| ^("OFF") ***********************************/ func Off(e:FrameEvent) { if (e._msg == "turnOn") { // |turnOn| _state = On // ->> $On return // ^ } if (e._msg == "toggle") { // |toggle| _state = On // ->> $On return // ^ } if (e._msg == "status") { // |status| e._return = "OFF" return // ^("OFF") } } /********************************** $On |turnOff| ->> $Off ^ |toggle| ->> $Off ^ |status| ^("ON") ***********************************/ func On(e:FrameEvent) { if (e._msg == "turnOff") { // |turnOff| _state = Off // ->> $Off return // ^ } if (e._msg == "toggle") { // |toggle| _state = Off // ->> $Off return // ^ } if (e._msg == "status") { // |status| e._return = "ON" return // ^("ON") } } } 

Der letzte Schritt in der Entwicklung unseres Schalters zeigt, wie einfach es ist, den Zweck des FMN-Controllers zu verstehen. Relevanter Code zeigt, wie einfach die Implementierung mithilfe von Frame-Mechanismen ist.

Nachdem wir die Switch-Maschine erstellt haben, können wir das Feuer einschalten und mit dem Kochen beginnen!

Klangzustand


Ein wichtiger, wenn auch subtiler Aspekt von Automaten ist, dass der aktuelle Zustand der Maschine entweder das Ergebnis einer Situation (z. B. Einschalten) oder einer Art Analyse von Daten oder der Umgebung ist. Wenn die Maschine in den gewĂŒnschten Zustand wechselt, ist dies impliziert. dass sich die Situation ohne das Wissen des Autos nicht Ă€ndern wird.

Diese Annahme ist jedoch nicht immer richtig. In einigen Situationen ist eine ÜberprĂŒfung (oder "Erfassung") der Daten erforderlich, um den aktuellen logischen Zustand zu bestimmen:

  1. anfÀnglicher wiederhergestellter Zustand - wenn die Maschine aus einem konstanten Zustand wiederhergestellt wird
  2. externer Zustand - Definiert die „tatsĂ€chliche Situation“, die zum Zeitpunkt der Erstellung, Wiederherstellung oder des Betriebs der Maschine in der Umgebung besteht
  3. FlĂŒchtiger interner Zustand - Wenn sich ein Teil der internen Daten, die von einer laufenden Maschine verwaltet werden, außerhalb der Kontrolle der Maschine Ă€ndern kann

In all diesen FĂ€llen mĂŒssen Daten, Umgebung oder beides „geprĂŒft“ werden, um die Situation zu bestimmen und den Zustand der Maschine entsprechend einzustellen. Idealerweise kann diese Boolesche Logik in einer einzigen Funktion implementiert werden, die den korrekten logischen Zustand definiert. Um dieses Muster zu unterstĂŒtzen, verfĂŒgt die Frame-Notation ĂŒber eine spezielle Art von Funktion, die das Universum untersucht und die aktuelle Situation bestimmt. Solche Funktionen werden durch das PrĂ€fix $ vor dem Namen der Methode angegeben, die einen Link zum Status zurĂŒckgibt :

 $probeForState() 

In unserer Situation kann eine solche Methode wie folgt implementiert werden:

 func probeForState():FrameState { if (temp < 32) return Solid if (temp < 212) return Liquid return Gas } 

Wie wir sehen können, gibt die Methode einfach einen Verweis auf die Zustandsfunktion zurĂŒck, die dem korrekten logischen Zustand entspricht. Diese Erfassungsfunktion kann dann verwendet werden, um in den richtigen Zustand zu gelangen:

 ->> $probeForState() 

Der Implementierungsmechanismus sieht folgendermaßen aus:

 _state = probeForState() 

Die Zustandserfassungsmethode ist ein Beispiel fĂŒr die Rahmennotation zum Verwalten des Zustands auf eine bestimmte Weise. Als nĂ€chstes lernen wir auch die wichtige Notation fĂŒr die Verwaltung von FrameEvents.

Verhaltensvererbung und Dispatcher


Verhaltensvererbung und Dispatcher sind ein leistungsfÀhiges Programmierparadigma und das letzte Thema zur Frame-Notation in diesem Artikel.

Frame verwendet die Vererbung von Verhalten , nicht die Vererbung von Daten oder anderen Attributen. FĂŒr diesen Status werden FrameEvents an andere Status gesendet, wenn der Anfangsstatus das Ereignis nicht behandelt (oder, wie wir in den nĂ€chsten Artikeln sehen werden, es nur weitergeben möchte). Diese Ereigniskette kann bis zu einer beliebigen Tiefe reichen.

Zu diesem Zweck können Maschinen unter Verwendung einer Technik implementiert werden, die als Methodenverkettung bezeichnet wird . Die FMN-Notation zum Senden von Ereignissen von einem Status in einen anderen lautet Dispatcher => :

 $S1 => $S2 

Diese FMN-Anweisung kann wie folgt implementiert werden:

 func S1(e:FrameEvent) { S2(e) // $S1 => $S2 } 

Jetzt sehen wir, wie einfach es ist, Zustandsmethoden zu verketten. Wenden wir diese Technik auf eine ziemlich schwierige Situation an:

 #Movement -machine- $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) $AtAttention => $Motionless |isStanding| ^(true) $LyingDown => $Motionless |isStanding| ^(false) $Moving |isMoving| ^(true) $Motionless |getSpeed| ^(0) |isMoving| ^(false) 

Im obigen Code sehen wir, dass es zwei GrundzustĂ€nde gibt - $ Moving und $ Motionless - und die anderen fĂŒnf ZustĂ€nde erben wichtige Funktionen von ihnen. Die Ereignisbindung zeigt uns deutlich, wie die Bindungen im Allgemeinen aussehen werden:


Dank der Techniken, die wir gelernt haben, wird die Implementierung sehr einfach sein:

 class Movement { // #Movement // -machine- /********************************** $Walking => $Moving |getSpeed| ^(3) |isStanding| ^(true) ***********************************/ func Walking(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 3 return } if (e._msg == "isStanding") { e._return = true return } Moving(e) // $Walking => $Moving } /********************************** $Running => $Moving |getSpeed| ^(6) |isStanding| ^(true) ***********************************/ func Running(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 6 return } if (e._msg == "isStanding") { e._return = true return } Moving(e) // $Running => $Moving } /********************************** $Crawling => $Moving |getSpeed| ^(.5) |isStanding| ^(false) ***********************************/ func Crawling(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = .5 return } if (e._msg == "isStanding") { e._return = false return } Moving(e) // $Crawling => $Moving } /********************************** $AtAttention => $Motionless |isStanding| ^(true) ***********************************/ func AtAttention(e:FrameEvent) { if (e._msg == "isStanding") { e._return = true return } Motionless(e) // $AtAttention => $Motionless } /********************************** $LyingDown => $Motionless |isStanding| ^(false) ***********************************/ func LyingDown(e:FrameEvent) { if (e._msg == "isStanding") { e._return = false return } Motionless(e) // $AtAttention => $Motionless } /********************************** $Moving |isMoving| ^(true) ***********************************/ func Moving(e:FrameEvent) { if (e._msg == "isMoving") { e._return = true return } } /********************************** $Motionless |getSpeed| ^(0) |isMoving| ^(false) ***********************************/ func Motionless(e:FrameEvent) { if (e._msg == "getSpeed") { e._return = 0 return } if (e._msg == "isMoving") { e._return = false return } } } 

Wassermaschine


Jetzt verfĂŒgen wir ĂŒber die Grundlagen des Wissens ĂŒber FMN, sodass wir verstehen, wie die WaterSample-Klasse mit ZustĂ€nden und auf viel intelligentere Weise erneut implementiert werden kann. Wir werden es auch fĂŒr unseren Doktoranden nĂŒtzlich machen und ihm einen neuen $ Plasma-Status hinzufĂŒgen:


So sieht die vollstÀndige FMN-Implementierung aus:

 #WaterSample -machine- $Begin |create| // set temp to the event param value setTemp(@[temp]) // probe for temp state and change to it ->> $probeForState() ^ $Solid => $Default |melt| doMelt() ->> $Liquid ^ |sublimate| doSublimate() ->> $Gas ^ |getState| ^("SOLID") $Liquid => $Default |freeze| doFreeze() ->> $Solid ^ |vaporize| doVaporize() ->> $Gas ^ |getState| ^("LIQUID") $Gas => $Default |condense| doCondense() ->> $Liquid ^ |depose| doDepose() ->> $Solid ^ |ionize| doIonize() ->> $Plasma ^ |getState| ^("GAS") $Plasma => $Default |recombine| doRecombine() ->> $Gas ^ |getState| ^("PLASMA") $Default |melt| throw new InvalidStateError() |sublimate| throw new InvalidStateError() |freeze| throw new InvalidStateError() |vaporize| throw new InvalidStateError() |condense| throw InvalidStateError() |depose| throw InvalidStateError() |ionize| throw InvalidStateError() |recombine| throw InvalidStateError() |getState| throw InvalidStateError() 

Wie Sie sehen können, haben wir den Anfangszustand $ Begin, der auf die Nachricht | create | reagiert und behĂ€lt Wert temp. Die Erfassungsfunktion ĂŒberprĂŒft zuerst den Anfangswert temp, um den logischen Zustand zu bestimmen, und fĂŒhrt dann den Übergang der Maschine in diesen Zustand durch.

Alle physischen ZustĂ€nde ($ Solid, $ Liquid, $ Gas, $ Plasma) erben das Schutzverhalten vom $ Default-Zustand. Alle Ereignisse, die fĂŒr den aktuellen Status nicht gĂŒltig sind, werden an den Status $ Default ĂŒbergeben, der einen InvalidStateError-Fehler auslöst. Dies zeigt, wie einfache defensive Programmierung mithilfe der Verhaltensvererbung implementiert werden kann.

Und jetzt die Implementierung:

 class WaterSample { // -machine- var _state(e:FrameEvent) = Begin /********************************** $Begin |create| // set temp to the event param value setTemp(@[temp]) // probe for temp state and change to it ->> $probeForState() ^ ***********************************/ func Begin(e:FrameEvent) { if (e._msg == "create") { setTemp(e["temp"]) _state = probeForState() return } } /********************************** $Solid => $Default |melt| doMelt() ->> $Liquid ^ |sublimate| doSublimate() ->> $Gas ^ |sublimate| ^("SOLID") ***********************************/ func Solid(e:FrameEvent) { if (e._msg == "melt") { doMelt() _state = Liquid return } if (e._msg == "sublimate") { doSublimate() _state = Gas return } if (e._msg == "getState") { e._return = "SOLID" return } Default(e) } /********************************** $Liquid => $Default |freeze| doFreeze() ->> $Solid ^ |vaporize| doVaporize() ->> $Gas ^ |getState| ^("LIQUID") ***********************************/ func Liquid(e:FrameEvent) { if (e._msg == "freeze") { doFreeze() _state = Solid return } if (e._msg == "vaporize") { doVaporize() _state = Gas return } if (e._msg == "getState") { e._return = "LIQUID" return } Default(e) } /********************************** $Gas => $Default |condense| doCondense() ->> $Liquid ^ |depose| doDepose() ->> $Solid ^ |ionize| doIonize() ->> $Plasma ^ |getState| ^("GAS") ***********************************/ func Gas(e:FrameEvent) { if (e._msg == "condense") { doCondense() _state = Liquid return } if (e._msg == "depose") { doDepose() _state = Solid return } if (e._msg == "ionize") { doIonize() _state = Plasma return } if (e._msg == "getState") { e._return = "GAS" return } Default(e) } /********************************** $Plasma => $Default |recombine| doRecombine() ->> $Gas ^ |getState| ^("PLASMA") ***********************************/ func Plasma(e:FrameEvent) { if (e._msg == "recombine") { doRecombine() _state = Gas return } if (e._msg == "getState") { e._return = "PLASMA" return } Default(e) } /********************************** $Default |melt| throw new InvalidStateError() |sublimate| throw new InvalidStateError() |freeze| throw new InvalidStateError() |vaporize| throw new InvalidStateError() |condense| throw InvalidStateError() |depose| throw InvalidStateError() |ionize| throw InvalidStateError() |recombine| throw InvalidStateError() |getState| throw InvalidStateError() ***********************************/ func Default(e:FrameEvent) { if (e._msg == "melt") { throw new InvalidStateError() } if (e._msg == "sublimate") { throw new InvalidStateError() } if (e._msg == "freeze") { throw new InvalidStateError() } if (e._msg == "vaporize") { throw new InvalidStateError() } if (e._msg == "condense") { throw new InvalidStateError() } if (e._msg == "depose") { throw new InvalidStateError() } if (e._msg == "ionize") { throw new InvalidStateError() } if (e._msg == "recombine") { throw new InvalidStateError() } if (e._msg == "getState") { throw new InvalidStateError() } } } 

Fazit


Automaten sind ein Grundkonzept der Informatik, das zu lange nur in speziellen Bereichen der Software- und Hardwareentwicklung eingesetzt wurde. Die Hauptaufgabe von Frame besteht darin, eine Notation zum Beschreiben von Automaten zu erstellen und einfache Muster zum Schreiben von Code oder „Mechanismen“ fĂŒr deren Implementierung festzulegen. Ich hoffe, dass die Frame-Notation die Art und Weise Ă€ndert, wie Programmierer Maschinen betrachten, und eine einfache Möglichkeit bietet, sie in alltĂ€glichen Programmieraufgaben in die Praxis umzusetzen und sie natĂŒrlich vor Spaghetti im Code zu schĂŒtzen.


Terminator isst Pasta (Foto von Mr. Suzuki)
In zukĂŒnftigen Artikeln werden wir basierend auf den Konzepten, die wir gelernt haben, eine noch grĂ¶ĂŸere Kraft und Ausdruckskraft der FMN-Notation schaffen. Im Laufe der Zeit werde ich die Diskussion auf das Studium der visuellen Modellierung ausweiten, das FMN umfasst und die Probleme unsicheren Verhaltens in modernen AnsĂ€tzen zur Softwaremodellierung löst.

Source: https://habr.com/ru/post/de446330/


All Articles