« État » est un terme de programmation courant utilisé par tous les développeurs à mesure qu'ils progressent du niveau de programmation débutant au niveau intermédiaire. Alors, que signifie exactement le terme "État" ?
En général, l'état d'un objet est simplement l'instantané actuel de l'objet ou d'une partie de celui-ci. Pendant ce temps, en informatique, l'état d'un programme est défini comme sa position par rapport aux entrées précédemment stockées. Dans ce contexte, le terme "état" est utilisé de la même manière qu'en science : l'état d'un objet, tel qu'un gaz, un liquide ou un solide, représente sa nature physique actuelle, et l'état d'un programme informatique reflète ses valeurs ou son contenu actuels.
Les entrées stockées sont conservées sous forme de variables ou de constantes dans un programme informatique. Lors de l'évaluation de l'état d'un programme, les développeurs peuvent examiner les valeurs contenues dans ces entrées. L'état du programme peut changer pendant son exécution - les variables peuvent changer et les valeurs de la mémoire peuvent changer. Une variable de contrôle, comme celle utilisée dans une boucle par exemple, change l'état du programme à chaque itération. L'examen de l'état actuel d'un programme peut être utilisé pour tester ou analyser la base de code.
Dans les systèmes plus simples, la gestion des états est fréquemment gérée avec des instructions if-else, if-then-else, try-catch ou des drapeaux booléens ; cependant, cela est inutile lorsqu'il y a trop d'états imaginables dans un programme. Ils peuvent conduire à un code maladroit et compliqué qui est difficile à comprendre, à maintenir et à déboguer.
Un inconvénient des clauses if-else ou des booléens est qu'ils peuvent devenir assez étendus, et l'ajout d'un autre état est difficile car cela nécessite de réécrire le code de nombreuses classes différentes. Supposons que vous souhaitiez créer un jeu comportant le menu principal, une boucle de jeu et un écran de fin.
Construisons un lecteur vidéo par exemple :
class Video: def __init__(self, source): self.source = source self.is_playing = False self.is_paused = False self.is_stopped = True # A video can only be played when paused or stopped def play(self): if not self.is_playing or self.is_paused: # Make the call to play the video self.is_playing = True self.is_paused = False else: raise Exception( 'Cannot play a video that is already playing.' ) # A video can only be paused when it is playing def pause(self): if self.is_playing: # Make the call to pause the video self.is_playing = False self.is_paused = True else: raise Exception( 'Cannot pause a video that is not playing' ) # A video can only be stopped when it is playing or paused def stop(self): if self.is_playing or self.is_paused: # Make the call to stop the video self.is_playing = False self.is_paused = False else: raise Exception( 'Cannot stop a video that is not playing or paused' )
L'extrait de code ci-dessus est une implémentation if-else d'une simple application de lecteur vidéo, où les trois états de base sont - lecture, pause et arrêt. Cependant, si nous essayons d'ajouter plus d'états, le code deviendra rapidement complexe, gonflé, répétitif et difficile à comprendre et à tester. Voyons à quoi ressemble le code lors de l'ajout d'un autre état 'rewind' :
class Video: def __init__(self, source): self.source = source self.is_playing = False self.is_paused = False self.is_rewinding = False self.is_stopped = True # A video can only be played when it is paused or stopped or rewinding def play(self): if self.is_paused or self.is_stopped or self.is_rewinding: # Make the call to play the video self.is_playing = True self.is_paused = False self.is_stopped = False self.is_rewinding = False else: raise Exception( 'Cannot play a video that is already playing.' ) # A video can only be paused when it is playing or rewinding def pause(self): if self.is_playing or self.is_rewinding: # Make the call to pause the video self.is_playing = False self.is_paused = True self.is_rewinding = False self.is_stopped = False else: raise Exception( 'Cannot pause a video that is not playing or rewinding' ) # A video can only be stopped when it is playing or paused or rewinding def stop(self): if self.is_playing or self.is_paused or self.is_rewinding: # Make the call to stop the video self.is_playing = False self.is_paused = False self.is_stopped = True self.is_rewinding = False else: raise Exception( 'Cannot stop a video that is not playing or paused or rewinding' ) # 4. A video can only be rewinded when it is playing or paused. def rewind(self): if self.is_playing or self.is_paused: # Make the call to rewind the video self.is_playing = False self.is_paused = False self.is_stopped = False self.is_rewinding = True else: raise Exception( 'Cannot rewind a video that is not playing or paused' )
Sans le modèle State, vous devriez examiner l'état actuel du programme dans tout le code, y compris les méthodes de mise à jour et de dessin. Si vous souhaitez ajouter un quatrième état, tel qu'un écran de paramètres, vous devrez mettre à jour le code de nombreuses classes distinctes, ce qui n'est pas pratique. C'est là que l'idée des machines à états devient utile.
Les machines à états ne sont pas un concept nouveau en informatique ; ils constituent l'un des modèles de conception de base utilisés dans le secteur des logiciels. Il est plus orienté système que orienté codage et est utilisé pour modéliser des cas d'utilisation.
Regardons un exemple concret simple de location d'un taxi via Uber :
L'écran 1 est le premier écran que tous les utilisateurs de ce cas d'utilisation voient, et il est autonome. L'écran 2 dépend de l'écran 1, et vous ne pourrez pas accéder à l'écran 2 tant que vous n'aurez pas fourni de données précises sur l'écran 1. De même, l'écran 3 dépend de l'écran 2, tandis que l'écran 4 dépend de l'écran 3. Si vous ne ni que votre chauffeur n'annule votre trajet, vous serez redirigé vers l'écran 4, où vous ne pourrez pas planifier un autre trajet tant que celui en cours n'est pas terminé.
Disons qu'il pleut beaucoup et qu'aucun chauffeur n'accepte votre trajet ou qu'aucun chauffeur disponible dans votre région n'est trouvé pour terminer votre trajet ; une notification d'erreur vous avertissant de l'indisponibilité du pilote s'affiche et vous restez sur l'écran 3. Vous pouvez toujours revenir à l'écran 2, à l'écran 1 et même au tout premier écran.
Vous vous trouvez à une étape différente du processus de réservation de taxi et vous ne pouvez passer au niveau suivant que si une action spécifiée à l'étape actuelle réussit. Par exemple, si vous entrez le mauvais emplacement sur l'écran 1, vous ne pourrez pas passer à l'écran 2 et vous ne pourrez pas passer à l'écran 3 à moins que vous ne choisissiez une option de voyage sur l'écran 2, mais vous pouvez revenez toujours à l'étape précédente sauf si votre voyage est déjà réservé.
Dans l'exemple ci-dessus, nous avons divisé le processus de réservation de taxi en plusieurs activités, chacune pouvant ou non être autorisée à appeler une autre activité en fonction du statut de la réservation. Une machine d'état est utilisée pour modéliser cela. En principe, chacune de ces étapes/états devrait être autonome, l'une n'invoquant la suivante qu'une fois l'étape en cours terminée, avec succès ou non.
En termes plus techniques, la machine d'état nous permet de décomposer une grande action compliquée en une succession d'activités séparées plus petites, comme l'activité de réservation de taxi dans l'exemple précédent.
Les événements connectent des tâches plus petites et le passage d'un état à un autre est appelé transition. Nous effectuons normalement certaines actions après le passage d'un état à un autre, telles que la création d'une réservation dans le backend, l'émission d'une facture, l'enregistrement des données d'analyse des utilisateurs, la capture des données de réservation dans une base de données, le déclenchement du paiement une fois le voyage terminé, etc. .
Par conséquent, la formule générale d'une machine à états peut être donnée par :
État actuel + une action / un événement = un autre état
Voyons à quoi ressemblerait une machine d'état conçue pour une simple application de lecteur vidéo :
Et nous pouvons l'implémenter dans le code en utilisant des transitions comme suit :
from transitions import Machine class Video: # Define the states PLAYING = 'playing' PAUSED = 'paused' STOPPED = 'stopped' def __init__(self, source): self.source = source # Define the transitions transitions = [ # 1. A video can only be played when it is paused or stopped. {'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING}, # 2. A video can only be paused when it is playing. {'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED}, # 3. A video can only be stopped when it is playing or paused. {'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED}, ] # Create the state machine self.machine = Machine{ model = self, transitions = transitions, initial = self.STOPPED } def play(self): pass def pause(self): pass def stop(self): pass
Maintenant, au cas où nous voudrions ajouter un autre état, disons rembobiner, nous pouvons le faire facilement comme suit :
from transitions import Machine class Video: # Define the states PLAYING = 'playing' PAUSED = 'paused' STOPPED = 'stopped' REWINDING = 'rewinding' # new def __init__(self, source): self.source = source # Define the transitions transitions = [ # 1. A video can only be played when it is paused or stopped. {'trigger': 'play', 'source': self.PAUSED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.STOPPED, 'dest': self.PLAYING}, {'trigger': 'play', 'source': self.REWINDING, 'dest': self.PLAYING}, # new # 2. A video can only be paused when it is playing. {'trigger': 'pause', 'source': self.PLAYING, 'dest': self.PAUSED}, {'trigger': 'pause', 'source': self.REWINDING, 'dest': self.PAUSED}, # new # 3. A video can only be stopped when it is playing or paused. {'trigger': 'stop', 'source': self.PLAYING, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.PAUSED, 'dest': self.STOPPED}, {'trigger': 'stop', 'source': self.REWINDING, 'dest': self.STOPPED}, # new # 4. A video can only be rewinded when it is playing or paused. {'trigger': 'rewind', 'source': self.PLAYING, 'dest': self.REWINDING}, #new {'trigger': 'rewind', 'source': self.PAUSED, 'dest': self.REWINDING}, # new ] # Create the state machine self.machine = Machine{ model = self, transitions = transitions, initial = self.STOPPED } def play(self): pass def pause(self): pass def stop(self): pass def rewind(self): pass
Ainsi, nous pouvons voir comment les machines à états peuvent simplifier une implémentation complexe et nous éviter d'écrire du code incorrect. Après avoir appris les capacités des machines d'état, il est maintenant important de comprendre pourquoi et quand utiliser des machines d'état.
Les machines d'état peuvent être utilisées dans des applications qui ont des états distincts. Chaque étape peut conduire à un ou plusieurs états ultérieurs, ainsi qu'à la fin du flux de processus. Une machine d'état utilise une entrée utilisateur ou des calculs dans l'état pour choisir l'état à entrer ensuite.
De nombreuses applications nécessitent une étape "d'initialisation", suivie d'un état par défaut qui permet un large éventail d'actions. Les entrées précédentes et présentes, ainsi que les états, peuvent tous avoir un impact sur les actions qui sont exécutées. Des mesures de nettoyage peuvent alors être effectuées lorsque le système est "arrêté".
Une machine à états peut nous aider à conceptualiser et à gérer ces unités de manière plus abstraite si nous pouvons décomposer une tâche extrêmement complexe en unités plus petites et indépendantes, où nous devons simplement décrire quand un état peut passer à un autre état et ce qui se passe lorsque la transition se produit. Nous n'avons pas besoin de nous préoccuper de la façon dont la transition se produit après la configuration. Après cela, nous n'avons qu'à penser quand et quoi, pas comment.
De plus, les machines d'état nous permettent de voir l'ensemble du processus d'état d'une manière très prévisible ; une fois les transitions définies, nous n'avons pas à nous soucier d'une mauvaise gestion ou de transitions d'état erronées ; la transition incorrecte ne peut se produire que si la machine d'état est configurée correctement. Nous avons une vue complète de tous les états et transitions d'une machine d'état.
Si nous n'utilisons pas de machine à états, nous sommes soit incapables de visualiser nos systèmes dans divers états possibles, soit nous associons sciemment ou inconsciemment nos composants étroitement ensemble, soit nous écrivons de nombreuses conditions if-else pour simuler des transitions d'état, ce qui complique les tests unitaires et d'intégration car il faut s'assurer que tous les cas de test sont écrits pour valider la possibilité de toutes les conditions et branchements utilisés.
Les machines à états, en plus de leur capacité à développer des algorithmes décisionnels, sont des formes fonctionnelles de planification d'applications. À mesure que les applications deviennent plus complexes, le besoin d'une conception efficace augmente.
Les diagrammes d'état et les organigrammes sont utiles et parfois essentiels tout au long du processus de conception. Les machines d'état sont importantes non seulement pour la planification des applications, mais elles sont également simples à créer.
Voici quelques-uns des principaux avantages des machines d'état dans l'informatique moderne :
Tout n'est pas bon dans les machines à états, elles peuvent parfois entraîner des inconvénients et des défis. Voici quelques-uns des problèmes courants avec les machines d'état :
Lorsque vous utilisez une machine d'état, votre système devrait idéalement avoir deux composants logiques :
La machine d'état peut être considérée comme l'infrastructure qui pilote les transitions d'état ; il vérifie les transitions d'état et exécute les actions configurées avant, pendant et après une transition ; cependant, il ne doit pas savoir quelle logique métier est effectuée dans ces actions.
Donc, en général, c'est une bonne idée d'isoler la machine d'état de la logique métier de base en utilisant des abstractions correctes ; sinon, gérer le code serait un cauchemar.
Voici quelques autres scénarios réels dans lesquels nous devons utiliser la logique de la machine d'état avec prudence :
Voici quelques-unes des applications pratiques qui bénéficient du concept de machines à états dans notre vie quotidienne :
Lorsque vous achetez quelque chose sur un site de commerce électronique en ligne, par exemple, il passe par différentes phases, telles que Commandé, Emballé, Expédié, Annulé, Livré, Payé, Remboursé, etc. La transition se produit automatiquement lorsque les choses se déplacent dans un entrepôt ou un centre logistique et sont scannées à différentes étapes, par exemple lorsqu'un utilisateur annule ou souhaite un remboursement.
La notion de machines à états est extrêmement utile en programmation. Cela rationalise non seulement le processus de développement d'applications de cas d'utilisation plus complexes, mais réduit également le travail de développement nécessaire. Il offre une compréhension plus simple et élégante des événements modernes et, lorsqu'il est correctement appliqué, peut faire des miracles.