Comment ne pas se tirer une balle dans la jambe avec une machine d'état

La machine à états est rarement utilisée par les développeurs mobiles. Bien que la majorité connaisse les principes du travail et les mette en œuvre facilement de manière indépendante. Dans l'article, nous allons déterminer quelles tâches sont résolues par la machine d'état en utilisant l'exemple des applications iOS. L'histoire est appliquée dans la nature et est consacrée aux aspects pratiques du travail.

Sous la coupe, vous trouverez une transcription étendue du discours d'Alexander Sychev ( Brain89 ) sur AppsConf , dans laquelle il a partagé ses options pour utiliser la machine d'état dans le développement d'applications non-jeu.


À propos du conférencier: Alexander Sychev a été engagé dans le développement iOS pendant huit ans, au cours desquels il a participé à la création d'applications simples et de clients complexes pour les réseaux sociaux et le secteur financier. À l'heure actuelle, est un responsable technique chez Sberbank.

Ils viennent à la programmation de nombreux domaines, ayant des antécédents et des expériences différents, donc nous rappelons d'abord la théorie de base.

Énoncé du problème




Une machine à états est une abstraction mathématique qui se compose de trois éléments principaux:

  • de nombreux états internes
  • l'ensemble des signaux d'entrée qui déterminent la transition de l'état actuel au suivant,
  • ensembles d'états finaux, à la transition vers lesquels l'automate termine son travail ("admet le mot d'entrée x").

Condition


Par état, nous entendons une variable ou un groupe de variables qui déterminent le comportement d'un objet. Par exemple, dans l'application iOS standard «Paramètres », il y a un élément «Gras» («Basique → Accès universel»). La valeur de cet élément vous permet de basculer entre deux options pour afficher du texte sur l'écran de l'appareil.



En envoyant le même signal «Changer la valeur de l'interrupteur à bascule » , nous obtenons une réaction différente du système: soit le style de police habituel soit en gras - tout est simple. Étant dans des états différents et recevant le même signal, un objet réagit différemment à un changement d'état.

Tâches traditionnelles


En pratique, les programmeurs rencontrent souvent une machine à états finis.

Applications de jeux


C'est la première chose qui me vient à l'esprit - dans le cadre du gameplay, presque tout est déterminé par l'état actuel du jeu. Ainsi, Apple suppose l'utilisation de machines d'état principalement dans les applications de jeu (nous en discuterons en détail plus tard).

Le comportement du système lors du traitement du même signal mais avec un état interne différent peut être illustré par les exemples suivants. Par exemple:

● le personnage du jeu peut être de différentes forces: l'un en armure mécanique et avec un pistolet laser, et l'autre est faiblement pompé. En fonction de cet état, le comportement des ennemis est déterminé: ils attaquent ou s'enfuient.



● le jeu est en pause - pas besoin de dessiner l'image actuelle; le joueur en mode menu ou en cours de jeu - le rendu est complètement différent.

Analyse de texte


L'une des tâches d'analyse de texte les plus courantes associées à l'utilisation d'une machine d'état est les filtres anti-spam. Soit un ensemble de mots vides et une séquence d'entrée. Vous devez soit filtrer cette séquence, soit ne pas l'afficher du tout.



Formellement, c'est la tâche de trouver une sous-chaîne dans une chaîne. Pour le résoudre, l'algorithme Knut-Morris-Pratt est utilisé, dont l'implémentation logicielle est une machine à états finis. L'état est le décalage de la séquence d'entrée et le nombre de caractères trouvés dans le motif - mot d'arrêt.

De plus, lors de l'analyse d'expressions régulières , des machines à états finis sont souvent utilisées.

Traitement parallèle des requêtes


Une machine d'état est l'une des options pour implémenter le traitement des demandes et exécuter un ensemble strict d'instructions.



Par exemple, dans le serveur Web nginx, les demandes d'entrée de divers protocoles sont traitées à l'aide de machines d'état. Selon le protocole spécifique, une implémentation spécifique de la machine à états est sélectionnée et, par conséquent, un ensemble d'instructions bien connu est exécuté.

En général, deux classes de problèmes sont obtenues:

  • gérer la logique d'un objet complexe avec un état interne complexe,
  • formation des flux de contrôle et de données (description de l'algorithme).

De toute évidence, ces tâches courantes sont rencontrées dans la pratique de tout programmeur. Par conséquent, l'utilisation d'une machine d'état est possible, y compris dans les applications de contenu non liées aux jeux, qui sont impliquées dans la plupart des développeurs mobiles.

Ensuite, nous analyserons où et quand la machine d'état peut être utilisée pour créer des applications iOS typiques.

La plupart des applications mobiles ont une architecture en couches. Il existe trois couches de base.

  • Couche de présentation.
  • Couche logique métier.
  • Un ensemble d'aides, de clients réseau, etc. (couche principale).

Comme indiqué ci-dessus, la machine à états contrôle les objets ayant un comportement complexe, c'est-à-dire avec un état complexe. Ces objets sont définitivement dans la couche de présentation, car il prend des décisions en traitant les entrées utilisateur ou les messages du système d'exploitation. Examinons les différentes approches de son exécution.



Dans la métaphore architecturale classique de Model-View-Controller, l'état sera dans le contrôleur: il décide ce qui est affiché dans la vue et comment répondre aux signaux d'entrée: en appuyant sur un bouton, en changeant le curseur, etc. Il est logique que l'une des implémentations du contrôleur soit une machine à états.



Dans VIPER, l'état est dans le présentateur: c'est lui qui détermine la transition de navigation spécifique à partir de l'écran courant et l'affichage des données dans la Vue.



Dans Model-View-ViewModel, l'état est dans le ViewModel. Que nous ayons ou non des liants réactifs, le comportement du module défini dans la métaphore MVVM sera enregistré dans le ViewModel. De toute évidence, sa mise en œuvre via une machine à états est une option acceptable.



Des objets complexes avec un ensemble d'états non triviaux sont également rencontrés sur la couche logique métier de l'application. Par exemple, un client réseau, qui, selon qu'une connexion au serveur est établie ou non, envoie ou bloque les demandes. Ou un objet pour travailler avec une base de données qui doit traduire des fonctions de langage en une requête SQL, l'exécuter, obtenir une réponse, la traduire en objets, etc.



Dans des tâches plus spécifiques, comme un module de paiement, dans lequel un ensemble d'états plus large, une logique complexe, l'utilisation d'une machine d'état est également correcte.

En conséquence, nous constatons que dans les applications mobiles, il existe de nombreux objets dont l'état et la logique de comportement sont décrits plus compliqués qu'avec une seule phrase. Ils doivent être capables de gérer.

Prenons un exemple réel et comprenez à quel point une machine à états finis est vraiment nécessaire et où son application n'est pas justifiée.



Considérez le ViewController de l'application iOS Championship, une ressource sportive populaire. Ce contrôleur affiche un ensemble de commentaires sous forme de tableau. Les utilisateurs entrent la description du match, regardent les photos, lisent les nouvelles et laissent leurs commentaires. L'écran est assez simple: la couche sous-jacente donne des données, elles sont traitées et affichées à l'écran.


Des données réelles ou une erreur peuvent être transmises à l'écran. Ainsi, le premier opérateur conditionnel apparaît, la première branche, qui détermine le comportement ultérieur de l'application.

La question suivante est de savoir quoi faire s'il n'y a pas de données. Cette condition est-elle une erreur? Probablement pas: toutes les nouvelles ne contiennent pas de commentaires des utilisateurs. Par exemple, le hockey en Égypte intéresse peu de monde; dans un tel article, il n'y a généralement pas de commentaires. Il s'agit d'un comportement normal et de l'état normal de l'écran que vous devez pouvoir afficher. Ainsi, le deuxième opérateur conditionnel apparaît.

Il est logique de supposer qu'il existe également un état de démarrage dans lequel l'utilisateur attend des données (par exemple, lorsque l'écran de commentaire n'apparaît qu'à l'écran). Dans ce cas, affichez correctement l'indicateur de chargement. Il s'agit de la troisième déclaration conditionnelle.

Nous avons donc déjà quatre états sur un seul écran, dont la logique d'affichage est décrite par la construction if-else-if-else.



Mais que se passe-t-il s'il y en a plus? Le développement itératif de l'écran conduit à un enchevêtrement complexe de constructions conditionnelles, à un tas de drapeaux ou à un boîtier de commutation multiple encombrant. Ce code fait peur. Imaginez que le développeur qui le soutiendra sache où vous vivez et qu'il a une tronçonneuse qu'il porte toujours avec lui. Et vous voulez vraiment vivre votre petite pension bien méritée.

Je pense que dans ce cas, il vaut la peine de considérer s'il vaut la peine de laisser une telle implémentation dans l'application.

Inconvénients


Comprenons ce que nous n'aimons pas à propos de ce code.



Tout d'abord, c'est difficile à lire .

Étant donné que le code est mal lu, cela signifie qu'il sera difficile pour un nouveau développeur de comprendre exactement ce qui est implémenté à un endroit particulier du projet. Par conséquent, cela passera beaucoup de temps à analyser la logique de comportement des applications - le coût du support et du développement augmentera .

Ce code n'est pas flexible . Si vous devez ajouter un nouvel état qui ne découle pas de l'échelle actuelle, il peut ne pas réussir du tout! Si vous avez besoin d'un passage traversant - arrêtez brusquement de passer les contrôles sur cette échelle - comment le faire? Presque rien.

De plus, avec cette approche, il n'y a aucune protection contre les états fictifs . Lorsque les transitions sont décrites via un boîtier de commutation, le comportement par défaut est très probablement implémenté. Cet état est logique en termes de comportement du programme, mais difficilement logique en termes de logique humaine ou métier de l'application.

Quelle peut être la solution aux lacunes indiquées? Bien sûr, c'est la construction de la logique de chaque module / contrôleur / objet complexe, non pas basée sur l'intuition, mais en utilisant une bonne approche formalisée. Par exemple, une machine à états finis.

Gameplaykit


À titre d' exemple, nous prenons ce que propose Apple. Dans le cadre du cadre GameplayKit, il existe deux classes qui nous aident à travailler avec la machine à états.

  • GKState.
  • GKStateMachine.

Le nom du cadre indique clairement qu'Apple voulait être utilisé dans les jeux. Mais dans les applications non ludiques, cela sera utile.



La classe GKState définit l'état. Pour le décrire, vous devez effectuer des étapes simples. Nous héritons de cette classe, définissons le nom de l'état et définissons trois méthodes.

  • isValidNextState - si l'état actuel est valide sur la base du précédent.
  • didEnterFrom - actions sur la transition vers cet état.
  • willExitTo - actions à la sortie de cet état.




GKStateMachine est une classe de machine à états. C’est encore plus simple. Il suffit d'effectuer deux actions.

  • Nous passons l'ensemble des états d'entrée au tableau typé via l'initialiseur.
  • Nous faisons des transitions en fonction des signaux d'entrée en utilisant la méthode enter. Grâce à elle, l'état initial est également défini.

Il peut être déroutant que n'importe quelle classe soit passée comme argument à la méthode enter . Mais il convient de noter qu'un objet de n'importe quelle classe ne peut pas être défini dans un tableau d'états - cela interdit le typage strict. Par conséquent, si vous définissez une classe arbitraire comme classe d'état suivante, rien ne se produira et la méthode enter retournera false.

États et transitions entre eux


Après avoir pris connaissance du framework d'Apple, revenons à l'exemple. Il est nécessaire de décrire les états et les transitions entre eux. Vous devez le faire de la manière la plus compréhensible. Il existe deux options courantes: un tableau ou un graphique de transition. Le graphique de transition, à mon avis, est une option plus compréhensible. Il est en UML de manière standardisée. Par conséquent, nous le choisissons.

Dans le graphique de transition, il y a des états qui sont décrits par des noms et des flèches qui relient ces états pour décrire les transitions. Dans l'exemple, il y a un état initial - nous attendons des données - et il y a trois états qui peuvent être atteints à partir de celui initial: données reçues, pas de données et erreur.



Dans l'implémentation, nous obtenons quatre petites classes.



Analysons l'état "Données en attente". A l'entrée, il vaut la peine d'afficher l'indicateur de téléchargement. Et lorsque vous quittez cet état , cachez-le. Pour ce faire, vous devez avoir un lien faible vers le ViewController, qui est contrôlé par la machine d'état créée.



Paramètres machine


La deuxième étape à effectuer consiste à définir les paramètres de la machine d'état. Pour ce faire, créez des états et transférez-les vers l'objet automate.



Veillez également à définir l'état initial



En principe, tout, la machine est prête. Maintenant, il est nécessaire de traiter les réactions aux événements externes, en changeant l'état de l'automate.



Rappelez l'énoncé du problème. Nous avons obtenu une échelle de if-else, sur la base de laquelle il a été décidé de l'action à effectuer. En tant que contrôle pour un automate simple, une telle option d'implémentation peut être (en fait, un simple commutateur - il s'agit d'une implémentation primitive d'une machine à états finis), mais nous ne nous débarrassons pratiquement pas des inconvénients mentionnés précédemment.

Il existe une autre approche qui vous permettra de vous éloigner de ces échelles. Il est proposé par les classiques de la programmation - le soi-disant «gang de quatre».



Il existe un modèle de conception spécial, appelé «État».



Il s'agit d'un modèle de comportement similaire à une stratégie qui décrit une abstraction de la machine à états. Il permet à l'objet de changer son comportement en fonction de l'état. Le but principal de l'application est d' encapsuler le comportement et les données associés à un état particulier dans une classe distincte. Ainsi, la machine à états, qui a initialement pris la décision de l'état à provoquer, va maintenant transmettre un signal, le traduire en état, et l'état prendra une décision. Déchargez donc partiellement l'échelle et le code deviendra plus agréable à utiliser.

Le cadre standard ne sait pas comment. Il suggère que GKStateMachine prendra la décision. Par conséquent, nous développons la machine à états finis avec une nouvelle méthode, où en tant que configuration, nous passons la description de toutes les variables conditionnelles qui déterminent de manière unique l'état suivant. Dans cette méthode, vous pouvez déléguer la sélection de l'état suivant à l'état actuel.



Il est recommandé de décrire l'état avec un seul objet et de toujours le transmettre, plutôt que d'écrire de nombreux paramètres d'entrée. Ensuite, nous déléguons le choix de l'état suivant à l'état actuel. C'est toute la mise à niveau.



Avantages de GameplayKit.

  • Bibliothèque standard. Pas besoin de télécharger quoi que ce soit, d'utiliser des cocoapodes ou des carthages.
  • La bibliothèque est assez facile à apprendre.
  • Il existe deux implémentations à la fois: sur Objective-C et sur Swift.

Inconvénients:

  • Les réalisations des états et des transitions sont étroitement liées.
    Le principe de la responsabilité exclusive est violé: l'État sait où il va et comment.
  • Les états en double ne sont en aucun cas contrôlés.
    Un tableau est transmis à la machine d'état, pas beaucoup d'états. Si vous transférez plusieurs états identiques, le dernier de la liste sera utilisé.

Quelles sont les autres implémentations de machines à états finis? Jetez un œil à GitHub.

Implémentations Objective-C




TransitionKit


Il s'agit de la bibliothèque Objective-C la plus populaire depuis longtemps, sans carences identifiées dans GamePlayKit. Il nous permet d'implémenter une machine à états et toutes les actions qui lui sont associées sur des blocs.

L'état est séparé des transitions .

Dans TransitionKit, il y a 2 classes.

  1. TKState - pour définir les états et les actions d'entrée et de sortie.
  2. TKEvent est une classe pour décrire la transition.
    TKEvent lie certains États à d'autres. L'événement lui-même est défini simplement par une chaîne.

De plus, il y a des avantages supplémentaires.

Vous pouvez transférer des données utiles pendant la transition . Cela fonctionne de la même manière que lorsque vous utilisez NSNotificationCenter. Toute charge utile utile se présente sous la forme d'un dictionnaire userInfo, et l'utilisateur analyse les informations.

La transition erronée a une description . Lorsque nous essayons de faire une transition inexistante - impossible - nous obtenons non seulement la valeur NO lors du retour de la méthode de transition, mais aussi une description détaillée de l'erreur, qui est utile lors du débogage d'une machine à états.



TransitionKit est utilisé dans la moissonneuse réseau populaire RestKit. Ceci est un assez bon exemple de la façon dont une machine d'état peut être utilisée dans le noyau d'application lors de l'implémentation des opérations réseau.



RestKit a une classe spéciale - RKOperationStateMachine - pour gérer les opérations simultanées. En entrée, il accepte l'opération en cours de traitement et la file d'attente pour son exécution.



En interne, la machine à états est très simple: trois états (prêt, exécuté, terminé) et deux transitions: début et fin d'exécution. Après le début du traitement (et à toutes les transitions), la machine d'état commence à contrôler un bloc de code utilisateur prédéfini dans la file d'attente spécifiée lors de la création de la file d'attente.

Une opération associée à son automate transfère des événements externes à l'automate et effectue des transitions entre les états et toutes les actions associées. La machine d'état prend soin de

  • exécution de code asynchrone,
  • exécution de code atomique lors des transitions,
  • contrôle de transition
  • Annulation des opérations
  • l'exactitude des variables d'état de changement de fonctionnement: isReady, isExecuting, isFinished.

Shift


En plus de TransitionKit, il convient de mentionner séparément Shift - une minuscule bibliothèque implémentée en tant que catégorie sur NSObject. Cette approche vous permet de transformer n'importe quel objet en machine à états, décrivant son état sous la forme de constantes de chaîne et d'actions en blocs pendant les transitions. Bien sûr, il s'agit davantage d'un projet de formation, mais assez intéressant et vous permet d'essayer ce qu'est une machine d'état à un coût minimal.

Implémentations rapides




Il existe de nombreuses implémentations de machines à états finis sur Swift. J'en citerai un ( remarque : malheureusement, le projet n'a pas évolué au cours des deux dernières années après le rapport, mais les idées qu'il contient méritent d'être racontées dans l'article).

SwiftyStateMachine


Dans SwiftyStateMachine, la machine à états est représentée par une structure non stable; grâce aux méthodes didSet de la propriété, vous pouvez facilement détecter les changements d'état.

Dans cette bibliothèque, la machine à états est définie à travers la table de correspondance des états et des transitions entre eux. Ce schéma est décrit séparément de l'objet que la machine contrôlera. Ceci est implémenté via un boîtier de commutation imbriqué.



, .

  • .
    , .
  • .
    state machine , state machine.
  • , , .
  • , DOT.
    state- — DOT. , , .



Conclusion


.

  • .
    , . , . , .
  • .
    ( ).
  • .
    , , .
  • . , SwiftyStateMachine , , . .
  • .
    , . , , . .

.



. , . , , switch case: , , — .



. . , . , , , . .



, , . .



. : , — .





«-» .



, . .



app coordinators — , , : , . , .

, app coordinator , state machine. . , app coordinators state machine, , , , . , , , . .



, state machine , , .

state machine , if-else. , .

Cette année, à Apps Conf 2018, qui se tiendra les 8 et 9 octobre, Alexander prévoit de discuter de cinq principes de base de la programmation orientée objet et des limites de leur applicabilité.

Pour plus de rapports sur le développement mobile, consultez notre chaîne YouTube . Et si vous souhaitez recevoir des informations sur les nouvelles transcriptions et les rapports intéressants, abonnez-vous à la newsletter .

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


All Articles