Machines automatiques contre le code de spaghetti


"J'adore les westerns spaghetti, je déteste le code spaghetti"

Le «code spaghetti» est une expression idéale pour décrire un logiciel qui est un chaos fumant d'un point de vue cognitif et esthétique. Dans cet article, je vais parler d'un plan en trois points pour détruire un code spaghetti:

  • Nous discutons pourquoi le code de spaghetti n'est pas si savoureux.
  • Présentation d'un nouveau regard sur ce que fait réellement le code.
  • Nous discutons de Frame Machine Notation (FMN) , qui aide les développeurs à démêler une boule de pâte.

Nous savons tous combien il est difficile de lire le code de quelqu'un d'autre. Cela peut être dû au fait que la tâche elle-même est difficile ou parce que la structure du code est trop ... "créative". Souvent, ces deux problèmes vont de pair.

Les défis sont des tâches difficiles, et rien d'autre qu'une découverte révolutionnaire ne peut généralement les simplifier. Cependant, il arrive que la structure logicielle elle-même ajoute une complexité inutile, et ce problème mérite d'être résolu.

La laideur du code spaghetti réside dans sa logique conditionnelle complexe. Et bien que la vie puisse être difficile à imaginer sans les nombreuses constructions difficiles si-alors-autre, cet article vous montrera une meilleure solution.


Pour illustrer la situation avec du code spaghetti, nous devons d'abord tourner ceci:


Pâtes croustillantes

En cela:


Al dente!

Commençons à cuisiner.

État implicite


Pour faire des pâtes, nous avons vraiment besoin d'eau pour cuisiner. Cependant, même un élément apparemment simple impliquant du code spaghetti peut être très déroutant.

Voici un exemple simple:

(temp < 32) 

Que fait vraiment ce contrôle? De toute évidence, il divise la droite numérique en deux parties, mais que signifient ces parties? Je pense que vous pouvez faire une hypothèse logique, mais le problème est que le code ne le communique pas explicitement .

Si je confirme vraiment qu'elle vérifie si l'eau est SOLIDE [env. voie: selon l'échelle Fahrenheit, l'eau gèle à +32 degrés] , que signifiera logiquement le retour faux?

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

Bien que la vérification ait divisé les chiffres en deux groupes, il existe en fait trois états logiques - solide, liquide et gazeux (SOLIDE, LIQUIDE, GAZ)!

Autrement dit, cette ligne de nombre:


divisé par contrôle de condition comme suit:

 if (temp < 32) { 


 } else { 


 } 

Remarquez ce qui s'est passé car il est très important de comprendre la nature du code spaghetti. Une vérification booléenne a divisé l'espace numérique en deux parties, mais n'a PAS catégorisé le système comme une structure logique réelle à partir de (SOLID, LIQUID, GAS). Au lieu de cela, le chèque a divisé l'espace en (SOLIDE, tout le reste).

Voici une vérification similaire:

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

Visuellement, cela ressemblera à ceci:

 if (temp > 212) { 


 } else { 


 } 

Notez que:

  1. l'ensemble complet des états possibles n'est annoncé nulle part
  2. nulle part dans les constructions conditionnelles ne sont déclarés des états logiques ou des groupes d'états vérifiables
  3. certains États sont indirectement regroupés par la structure de la logique conditionnelle et de la ramification

Un tel code est fragile, mais très courant et pas assez volumineux pour causer des problèmes avec son support. Alors, aggravons la situation.


Je n'ai jamais aimé ton code de toute façon

Le code ci-dessus implique l'existence de trois états de la matière - SOLIDE, LIQUIDE, GAZ. Cependant, selon les données scientifiques, il existe en fait quatre états observables dans lesquels le plasma (PLASMA) est inclus (en fait, il y en a beaucoup d'autres, mais cela nous suffira). Bien que personne ne prépare une pâte à partir de plasma, si ce code est publié sur Github, puis qu'un étudiant de troisième cycle en physique des hautes énergies le bifurquera, nous devrons également maintenir cet état.

Cependant, lorsque du plasma est ajouté, le code ci-dessus fera naïvement ce qui suit:

 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) } 

Il est probable que l'ancien code, ajouté à de nombreux états du plasma, se cassera dans les autres branches. Malheureusement, rien dans la structure du code n'aide à signaler l'existence d'un nouvel état ou à influencer les changements. De plus, tous les bugs sont susceptibles d'être discrets, c'est-à-dire que les trouver sera le plus difficile. Dites simplement non aux insectes dans les spaghettis.

En bref, le problème est le suivant: les vérifications booléennes sont utilisées pour déterminer indirectement des états. Les états logiques ne sont souvent pas déclarés et ne sont pas visibles dans le code. Comme nous l'avons vu ci-dessus, lorsque le système ajoute de nouveaux états logiques, le code existant peut se casser. Pour éviter cela, les développeurs doivent réexaminer chaque vérification conditionnelle individuelle et chaque branche pour s'assurer que les chemins de code sont toujours valides pour tous leurs états logiques! C'est la principale raison de la dégradation des gros fragments de code à mesure qu'ils deviennent plus complexes.

Bien qu'il n'y ait aucun moyen de se débarrasser complètement des vérifications de données conditionnelles, toute technique qui les minimise réduira la complexité du code.

Jetons maintenant un œil à une implémentation orientée objet typique d'une classe qui crée un modèle très simple du volume d'eau. La classe gérera les changements dans l'état de la substance de l'eau. Après avoir étudié les problèmes de la solution classique à ce problème, nous discutons ensuite d'une nouvelle notation appelée Frame et montrons comment elle peut faire face aux difficultés que nous avons découvertes.

Portez d'abord l'eau à ébullition ...


La science a donné des noms à toutes les transitions possibles qu'une substance peut effectuer lorsque la température change.


Notre cours est très simple (et pas particulièrement utile). Il répond aux défis d'effectuer des transitions entre les états et modifie la température jusqu'à ce qu'elle devienne adaptée à l'état cible souhaité:

(Remarque: j'ai écrit ce pseudo-code. Utilisez-le dans votre travail uniquement à vos risques et périls.)

 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" } } 

Par rapport au premier exemple, ce code présente certaines améliorations. Tout d'abord, les nombres «magiques» codés en dur (32, 212) sont remplacés par les constantes des limites de température des états (WATER_SOLID_TEMP, WATER_GAS_TEMP). Ce changement commence à rendre les États plus explicites, quoique indirectement.

Des vérifications de «programmation défensive» apparaissent également dans ce code, ce qui restreint l'appel de méthode s'il n'est pas dans le bon état pour l'opération. Par exemple, l'eau ne peut pas geler si ce n'est pas un liquide - cela viole la loi (de la nature). Mais l'ajout de conditions de surveillance rend la compréhension de l'objectif du code plus difficile. Par exemple:

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

Cette vérification conditionnelle effectue les opérations suivantes:

  1. Vérifie si la temp inférieure à la température limite GAS
  2. Vérifie si la temp dépasse la température limite SOLIDE
  3. Renvoie une erreur si l'une de ces vérifications n'est pas vraie

Cette logique prête à confusion. Tout d'abord, être à l'état liquide est déterminé par ce que la substance n'est pas - un solide ou un gaz.

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

Deuxièmement, le code vérifie si l'eau est liquide pour savoir si une erreur doit être renvoyée.

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

La première fois pour comprendre cette double négation des états n'est pas facile. Voici une simplification qui réduit légèrement la complexité de l'expression:

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

Ce code est plus facile à comprendre car l'état isLiquidWater est explicite .

Nous explorons maintenant des techniques qui fixent un état explicite comme le meilleur moyen de résoudre des problèmes. Avec cette approche, les états logiques du système deviennent la structure physique du logiciel, ce qui améliore le code et simplifie sa compréhension.

Notation de trame machine


Frame Machine Notation (FMN) est un langage spécifique au domaine (Domain Specific Language, DSL) qui définit une approche catégorique, méthodologique et simple pour définir et implémenter divers types de machines . Par souci de simplicité, j'appellerai les automates Frame simplement des «machines», car cette notation peut définir des critères théoriques pour différents types (machines d'état, automates de stockage et évolution des automates - Turing machines). Pour connaître les différents types de machines et leur application, je recommande d'étudier la page sur Wikipédia .

Bien que la théorie des automates puisse être intéressante (une déclaration TRÈS douteuse), dans cet article, nous nous concentrerons sur l'application pratique de ces concepts puissants pour construire des systèmes et écrire du code.

Pour résoudre ce problème, Frame introduit une notation standardisée qui fonctionne à trois niveaux intégrés:

  1. DSL texte pour définir les contrôleurs Frame avec une syntaxe élégante et concise
  2. Un ensemble de modèles de codage de référence pour implémenter des classes orientées objet sous la forme de machines que Frame appelle des «contrôleurs»
  3. Notation visuelle dans laquelle FMN est utilisé pour exprimer des opérations complexes qui sont difficiles à représenter graphiquement - Frame Visual Notation (FVN)

Dans cet article, j'examinerai les deux premiers points: FMN et modèles de référence, et je laisserai la discussion sur FVN pour les prochains articles.

Le cadre est une notation qui a plusieurs aspects importants:

  1. FMN possède des objets de premier niveau liés au concept d'automates, qui ne sont pas disponibles dans les langages orientés objet.
  2. La spécification FMN définit des modèles d'implémentation standard dans un pseudo-code qui montrent comment la notation FMN peut être implémentée.
  3. FMN pourra bientôt compiler (travaux en cours) dans n'importe quel langage orienté objet

Remarque: l'implémentation de référence est utilisée pour démontrer l'équivalence absolue de la notation FMN et un moyen simple de l'implémenter dans n'importe quel langage orienté objet. Vous pouvez choisir n'importe quelle méthode.

Je vais maintenant vous présenter les deux objets de premier niveau les plus importants de Frame - Frame Events et Frame Controllers .

Événements de cadre


FrameEvents fait partie intégrante de la simplicité de la notation FMN. Un FrameEvent est implémenté en tant que structure ou classe qui a au moins les variables membres suivantes:

  • identifiant du message
  • dictionnaire ou liste de paramètres
  • objet de retour

Voici le pseudocode de la classe FrameEvent:

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

La notation de trame utilise le symbole @ , qui identifie l'objet FrameEvent. Chacun des attributs FrameEvent requis possède un jeton spécial pour y accéder:

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

Souvent, nous n'avons pas à spécifier avec quoi FrameEvent fonctionne. Étant donné que la plupart des contextes ne fonctionnent qu'avec un seul FrameEvent à la fois, la notation peut certainement être simplifiée afin qu'elle n'utilise que des sélecteurs d'attributs. Par conséquent, nous pouvons simplifier l'accès:

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

Une telle notation peut sembler étrange au début, mais nous verrons bientôt comment une syntaxe aussi simple pour les événements simplifie grandement la compréhension du code FMN.

Contrôleurs de trames


Un Frame Controller est une classe orientée objet, ordonnée de manière bien définie pour implémenter une machine Frame. Les types de contrôleurs sont identifiés par le préfixe # :

 #MyController 

cela équivaut au pseudocode orienté objet suivant:

 class MyController {} 

Évidemment, cette classe n'est pas particulièrement utile. Pour qu'il puisse faire quelque chose, le contrôleur a besoin d'au moins un état pour répondre aux événements.

Les contrôleurs sont structurés de manière à contenir des blocs de différents types, identifiés par un tiret entourant le nom du type de bloc:

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

Un contrôleur ne peut avoir plus d'une instance de chaque bloc et les types de bloc ne peuvent contenir que certains types de sous-composants. Dans cet article, nous examinons uniquement le bloc -machine- , qui ne peut contenir que des états. Les états sont identifiés par le jeton $ prefix.

Ici, nous voyons le FMN pour un contrôleur contenant une machine avec un seul état:

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

Voici l'implémentation du code FMN ci-dessus:

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

La mise en œuvre du bloc machine se compose des éléments suivants:

  1. variable _state , qui fait référence à une fonction de l'état actuel. Il est initialisé avec la première fonction d'état dans le contrôleur.
  2. une ou plusieurs méthodes d'état

La méthode State Frame est définie comme une fonction avec la signature suivante:

 func MyState(e:FrameEvent); 

Après avoir défini ces bases de l'implémentation du bloc machine, nous pouvons voir dans quelle mesure l'objet FrameEvent interagit avec la machine.

Unité d'interface


L'interaction de FrameEvents qui contrôlent le fonctionnement de la machine est l'essence même de la simplicité et de la puissance de la notation Frame. Cependant, nous n'avons pas encore répondu à la question, d'où viennent les FrameEvents - comment entrent-ils dans le contrôleur pour le contrôler? Une option: les clients externes eux-mêmes peuvent créer et initialiser FrameEvents, puis appeler directement la méthode pointée par la variable membre _state:

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

Une bien meilleure alternative serait de créer une interface commune qui encapsule un appel direct à la variable membre _state:

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

Cependant, le moyen le plus simple, correspondant à la manière habituelle de créer un logiciel orienté objet, est de créer des méthodes courantes qui envoient un événement au nom du client à la machine interne:

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

Frame définit la syntaxe d' un bloc d'interface qui contient des méthodes qui transforment les appels en une interface commune pour FrameEvents.

 #MyController -interface- buttonClick ... 

Le bloc d' interface possède de nombreuses autres fonctionnalités, mais cet exemple nous donne une idée générale de son fonctionnement. Je donnerai plus d'explications dans les articles suivants de la série.

Continuons maintenant à étudier le fonctionnement de l'automate Frame.

Gestionnaires d'événements


Bien que nous ayons montré comment définir une voiture, nous n'avons pas encore de notation avec laquelle faire quoi que ce soit. Pour traiter les événements, nous devons 1) être en mesure de sélectionner l'événement à traiter et 2) le rattacher au comportement en cours.

Voici un contrôleur Frame simple qui fournit l'infrastructure pour gérer les événements:

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

Comme indiqué ci-dessus, pour accéder à l'attribut _msg de l'événement _msg , la notation FMN utilise des crochets à partir de lignes verticales:

 |messageName| 

FMN utilise également un jeton d'exposant représentant l'instruction de retour. Le contrôleur illustré ci-dessus sera implémenté comme suit:

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

Nous voyons ici à quel point la notation FMN correspond clairement à un modèle d'implémentation facile à comprendre et à coder.

Après avoir défini ces aspects de base des événements, contrôleurs, machines, états et gestionnaires d'événements, nous pouvons procéder à la résolution de problèmes réels avec leur aide.

Machines à foyer unique


Ci-dessus, nous avons examiné un contrôleur apatride qui était assez inutile.

 #MyController 

Une étape plus haut dans la chaîne alimentaire d'utilité est une classe à un seul état qui, bien que non inutile, est simplement ennuyeuse. Mais au moins, il fait au moins quelque chose .

Voyons d'abord comment une classe avec un seul état (implicite) sera implémentée:

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

Aucun état n'est déclaré ou même implicite ici, mais supposons que si le code fait quelque chose, le système est dans l'état "Working".

Nous introduirons également une idée importante: les appels d'interface seront considérés comme similaires à l'envoi d'un événement à un objet. Par conséquent, le code ci-dessus peut être considéré comme une méthode de transmission du | statut | la classe Mono, toujours à l'état $ Working.

Cette situation peut être visualisée à l'aide de la table de liaison d'événements:


Examinons maintenant FMN, qui présente les mêmes fonctionnalités et correspond à la même table de liaison:

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

Voici à quoi ressemble l'implémentation:

 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") } } } 

Vous pouvez remarquer que nous avons également introduit une nouvelle notation pour l' instruction return , ce qui signifie évaluer l'expression et renvoyer le résultat à l'interface:

 ^(return_expr) 

Cet opérateur est équivalent

 @^ = return_expr 

ou juste

 ^ = return_expr 

Tous ces opérateurs sont fonctionnellement équivalents et vous pouvez utiliser n'importe lequel d'entre eux, mais ^(return_expr) semble le plus expressif.

Allumez le poêle


Jusqu'à présent, nous avons vu un contrôleur avec 0 états et un contrôleur avec 1 état. Ils ne sont pas encore très utiles, mais nous sommes déjà au bord de quelque chose d'intéressant.

Pour cuire nos pâtes, vous devez d'abord allumer le poêle. Voici une classe Switch simple avec une seule variable booléenne:

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

Bien que cela ne soit pas évident à première vue, le code ci-dessus implémente le tableau suivant de liaisons d'événements:


A titre de comparaison, voici une FMN pour le même comportement:

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

Nous voyons maintenant comment exactement la notation Frame correspond à l'objectif de notre code - attacher un événement (appel de méthode) à un comportement basé sur l'état dans lequel se trouve le contrôleur. De plus, la structure d'implémentation correspond également au tableau de liaison:

 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") } } } 

Le tableau vous permet de comprendre rapidement la fonction du contrôleur dans ses différents états. La structure de notation de trame et le modèle d'implémentation présentent des avantages similaires.

Cependant, notre commutateur a un problème fonctionnel notable. Il est initialisé dans l'état $ Off, mais ne peut pas basculer dans l'état $ On! Pour ce faire, nous devons saisir un opérateur de changement d'état .

Changer d'état


La déclaration de changement d'état est la suivante:

 ->> $NewState 

Maintenant, nous pouvons utiliser cet opérateur pour basculer entre $ Off et $ On:

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

Et voici la table de liaison d'événements correspondante:


Nouvel événement | bascule | déclenche désormais un changement qui passe simplement par les deux états. Comment mettre en œuvre une opération de changement d'état?

Nulle part est plus facile. Voici l'implémentation de 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") } } } 

Vous pouvez également apporter la dernière amélioration dans Switch2 afin qu'il vous permette non seulement de basculer entre les états, mais définit également explicitement l'état:

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

Contrairement à l'événement | toggle |, si | turnOn | transmis lorsque Switch3 est déjà activé ou | turnOff | lorsqu'il est déjà désactivé, le message est ignoré et rien ne se passe. Cette petite amélioration donne au client la possibilité d'indiquer explicitement l'état dans lequel le commutateur doit être:

 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") } } } 

La dernière étape de l'évolution de notre commutateur montre à quel point il est facile de comprendre l'objectif du contrôleur FMN. Le code pertinent montre à quel point il est facile à implémenter à l'aide des mécanismes Frame.

Après avoir créé la machine Switch, nous pouvons allumer le feu et commencer à cuisiner!

État sonore


Un aspect clé, quoique subtil, des automates est que l'état actuel de la machine est le résultat soit d'une situation (par exemple, mise sous tension), soit d'une sorte d'analyse des données ou de l'environnement. Lorsque la machine est passée à l'état souhaité, cela est implicite. que la situation ne changera pas à l'insu de la voiture.

Cependant, cette hypothèse n'est pas toujours vraie. Dans certaines situations, une vérification (ou «détection») des données est requise pour déterminer l'état logique actuel:

  1. état initial restauré - lorsque la machine est restaurée à partir d'un état constant
  2. état externe - définit la «situation réelle» qui existe dans l'environnement au moment de la création, de la restauration ou du fonctionnement de la machine
  3. état interne volatile - lorsqu'une partie des données internes gérées par une machine en cours d'exécution peut changer en dehors du contrôle de la machine

Dans tous ces cas, les données, l'environnement ou les deux doivent être "sondés" afin de déterminer la situation et de définir l'état de la machine en conséquence. Idéalement, cette logique booléenne peut être implémentée dans une seule fonction qui définit l'état logique correct. Pour prendre en charge ce modèle, la notation Frame a un type spécial de fonction qui sonde l'univers et détermine la situation à l'heure actuelle. Ces fonctions sont indiquées par le préfixe $ avant le nom de la méthode qui renvoie un lien vers l'état :

 $probeForState() 

Dans notre situation, une telle méthode peut être mise en œuvre comme suit:

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

Comme nous pouvons le voir, la méthode renvoie simplement une référence à la fonction d'état correspondant à l'état logique correct. Cette fonction de détection peut ensuite être utilisée pour entrer dans l'état correct:

 ->> $probeForState() 

Le mécanisme de mise en œuvre ressemble à ceci:

 _state = probeForState() 

La méthode de détection d'état est un exemple de notation de trame pour gérer l'état d'une manière donnée. Ensuite, nous apprendrons également la notation importante pour la gestion des FrameEvents.

Héritage comportemental et répartiteur


L'héritage comportemental et le répartiteur sont un paradigme de programmation puissant et le dernier sujet sur la notation de trame dans cet article.

Frame utilise l'héritage du comportement , pas l'héritage des données ou d'autres attributs. Pour cet état, FrameEvents sont envoyés à d'autres états si l'état initial ne gère pas l'événement (ou, comme nous le verrons dans les prochains articles, veut juste le transmettre). Cette chaîne d'événements peut aller à n'importe quelle profondeur souhaitée.

Pour cela, les machines peuvent être implémentées en utilisant une technique appelée chaînage de méthode . La notation FMN pour l'envoi d'événements d'un état à un autre est le répartiteur => :

 $S1 => $S2 

Cette déclaration FMN peut être implémentée comme suit:

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

Nous voyons maintenant combien il est facile de chaîner des méthodes d'état. Appliquons cette technique à une situation assez difficile:

 #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) 

Dans le code ci-dessus, nous voyons qu'il y a deux états de base - $ Moving et $ Motionless - et les cinq autres états héritent d'eux des fonctionnalités importantes. La liaison d'événement nous montre clairement à quoi ressembleront les liaisons en général:


Grâce aux techniques que nous avons apprises, la mise en œuvre sera très simple:

 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 } } } 

Machine à eau


Nous avons maintenant les bases de la connaissance de FMN, ce qui nous permet de comprendre comment réimplémenter la classe WaterSample avec des états et de manière beaucoup plus intelligente. Nous le rendrons également utile à notre étudiant physicien diplômé et lui ajouterons un nouvel état $ Plasma:


Voici à quoi ressemble l'implémentation complète de FMN:

 #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() 

Comme vous pouvez le voir, nous avons l'état initial de $ Begin, qui répond au message | create | et conserve sa valeur temp. La fonction de détection vérifie d'abord la valeur initiale temppour déterminer l'état logique, puis effectue la transition de la machine vers cet état.

Tous les états physiques ($ solide, $ liquide, $ gaz, $ plasma) héritent du comportement protecteur de l'état $ par défaut. Tous les événements qui ne sont pas valides pour l'état actuel sont passés à l'état $ Default, ce qui génère une erreur InvalidStateError. Cela montre comment une programmation défensive simple peut être implémentée en utilisant l'héritage de comportement.

Et maintenant l'implémentation:

 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() } } } 

Conclusion


Les automates sont un concept de base de l'informatique qui n'a été utilisé pendant trop longtemps que dans des domaines spécialisés du développement de logiciels et de matériel. La tâche principale de Frame est de créer une notation pour décrire les automates et définir des modèles simples pour écrire du code ou des «mécanismes» pour leur implémentation. J'espère que la notation Frame changera la façon dont les programmeurs regardent les machines, fournissant un moyen facile de les mettre en pratique dans les tâches de programmation quotidiennes et, bien sûr, de les sauver des spaghettis dans le code.


Terminator mange des pâtes (photo de Suzuki san)
Dans les prochains articles, sur la base des concepts que nous avons appris, nous allons créer encore plus de puissance et d'expressivité de la notation FMN. Au fil du temps, j'élargirai la discussion à l'étude de la modélisation visuelle, qui comprend la FMN et résout les problèmes de comportement incertain dans les approches modernes de la modélisation logicielle.

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


All Articles