Dans cet article, je vais décrire l’utilisation de la machine à états (State Machine), montrer une implémentation simple, une implémentation utilisant le modèle State. Il convient de mentionner qu’il n’est pas souhaitable d’utiliser le modèle d’État s’il y a moins de trois États, car cela conduit généralement à une complexité inutile dans la lisibilité du code et à des problèmes de support associés – tout doit être modéré.

Seigneur des drapeaux
Supposons que nous développions un écran de lecteur vidéo pour le système multimédia d’un avion civil, le lecteur doit être capable de charger un flux vidéo, de le lire, de permettre à l’utilisateur d’arrêter le processus de téléchargement, de rembobiner et d’effectuer d’autres opérations habituelles pour un joueur.
Supposons que le lecteur ait mis en cache le morceau suivant du flux vidéo, vérifié qu’il y a suffisamment de morceaux pour la lecture, commencé à lire le fragment à l’utilisateur et continue en même temps à télécharger le suivant.
À ce stade, l’utilisateur revient au milieu de la vidéo, c’est-à-dire qu’il doit maintenant arrêter de lire le fragment en cours et commencer le chargement à partir d’une nouvelle position. Cependant, il existe des situations dans lesquelles cela ne peut pas être fait : l’utilisateur ne peut pas contrôler la lecture du flux vidéo pendant qu’on lui montre une vidéo sur la sécurité aérienne. Vérifions l’indicateur isSafetyVideoPlaying pour vérifier cette situation.
Le système doit également être capable de mettre en pause la vidéo en cours et de diffuser une alerte du capitaine et de l’équipage du navire via le lecteur. Ajoutons un autre indicateur isAnnouncementPlaying. De plus, il est obligatoire de ne pas mettre la lecture en pause lors de l’affichage de l’aide sur l’utilisation du lecteur, un autre indicateur est HelpPresenting.
Exemple de pseudocode du lecteur multimédia :
class MediaPlayer {
public var isHelpPresenting = false
public var isCaching = false
public var isMediaPlaying: Bool = false
public var isAnnouncementPlaying = false
public var isSafetyVideoPlaying = false
public var currentMedia: Media = null
fun play(media: Media) {
if isMediaPlaying == false, isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
if isCaching == false {
if isHelpPresenting == false {
media.playAfterHelpClosed()
}
else {
media.playAfterCaching()
}
}
}
fun pause() {
if isAnnouncementPlaying == false, isSafetyVideoPlaying == false {
currentMedia.pause()
}
}
}
L’exemple ci-dessus est difficile à lire et à maintenir en raison d’une grande variabilité (entropie). Cet exemple est basé sur mon expérience de travail avec la base de code de *de nombreux* projets qui n’utilisaient pas de machine à états.
Chaque case à cocher doit spécifiquement « contrôler » les éléments de l’interface et la logique métier de l’application ; le développeur, en ajoutant une autre case à cocher, doit être capable de jongler avec eux, en vérifiant et revérifiant le tout plusieurs fois avec toutes les options possibles.
En remplaçant dans la formule « 2 ^ nombre de cases à cocher », vous pouvez obtenir 2 ^ 6 = 64 options pour le comportement de l’application pour seulement 6 cases à cocher, toutes ces combinaisons de cases à cocher devront être cochées et gérées manuellement.
Du côté du développeur, l’ajout de nouvelles fonctionnalités avec un tel système ressemble à ceci :
– Nous devons ajouter la possibilité d’afficher la page du navigateur de la compagnie aérienne, et elle devrait être réduite comme pour les films si les membres de l’équipage annoncent quelque chose.
– D’accord, je vais le faire. (Oh putain, je vais devoir ajouter un autre drapeau et revérifier tous les endroits où les drapeaux se croisent, ça fait beaucoup de choses à changer !)
C’est aussi un point faible du système de drapeaux : ; apporter des modifications au comportement de l’application. Il est très difficile d’imaginer comment modifier rapidement et de manière flexible un comportement en fonction des indicateurs, si après avoir modifié un seul indicateur, vous devez tout revérifier. Cette approche du développement entraîne beaucoup de problèmes, une perte de temps et d’argent.
Entrez dans la machine
Si vous regardez attentivement les drapeaux, vous comprendrez qu’en fait nous essayons de traiter des processus spécifiques se produisant dans le monde réel. Nous les listons : mode normal, affichage d’une vidéo de sécurité, diffusion d’un message du capitaine ou des membres d’équipage. Pour chaque processus, un ensemble de règles est connu qui modifie le comportement de l’application.
Selon les règles du modèle de machine à états (machine à états), nous listerons tous les processus en tant qu’états dans l’énumération, ajouterons un concept tel qu’un état au code du joueur, implémenterons un comportement basé sur l’état en supprimant les combinaisons sur les drapeaux. Ainsi, nous réduirons les options de test exactement au nombre d’états.
Pseudocode :
enum MediaPlayerState {
mediaPlaying,
mediaCaching,
crewSpeaking,
safetyVideoPlaying,
presentingHelp
}
class MediaPlayer {
fun play(media: Media) {
media.play()
}
func pause() {
media.pause()
}
}
class MediaPlayerStateMachine {
public state: MediaPlayerState
public mediaPlayer: MediaPlayer
public currentMedia: Media
//.. init (mediaPlayer) etc
public fun set(state: MediaPlayerState) {
switch state {
case mediaPlaying:
mediaPlayer.play(currentMedia)
case mediaCaching, crewSpeaking,
safetyVideoPlaying, presentingHelp:
mediaPlayer.pause()
}
}
}
L’énorme différence entre un système de drapeaux et une machine à états réside dans l’entonnoir de commutation d’état logique dans la méthode set(state: ..), il vous permet de traduire la compréhension humaine de l’état en code de programme, sans avoir à jouer à la logique. jeux de conversion de drapeaux en états avec prise en charge ultérieure du code.
État du modèle
Ensuite, je montrerai la différence entre l’implémentation naïve de la machine à états et le modèle d’état. Imaginons que nous devions ajouter 10 états ; en conséquence, la classe des machines à états atteindrait la taille d’un objet divin, ce qui serait difficile et coûteux à maintenir. Bien sûr, cette implémentation est meilleure que l’implémentation du flag (avec le système de flag, le développeur se tirera une balle en premier, et sinon, voyant 2 ^ 10 = 1024 variations, QA se pendra, mais si les deux *ne le font pas remarquerez* la complexité de la tâche, alors l’utilisateur dont l’application est simple remarquera qu’il refusera de travailler avec une certaine combinaison de drapeaux)
S’il y a un grand nombre d’états, il est nécessaire d’utiliser le modèle State.
Ajoutons un ensemble de règles au protocole d’État :
protocol State {
func playMedia(media: Media, context: MediaPlayerContext)
func shouldCacheMedia(context: MediaPlayerContext)
func crewSpeaking(context: MediaPlayerContext)
func safetyVideoPlaying(context:MediaPlayerContext)
func presentHelp(context: MediaPlayerContext)
}
Déplaçons l’implémentation de l’ensemble de règles dans des états distincts, par exemple, le code d’un état :
class CrewSpeakingState: State {
func playMedia(context: MediaPlayerContext) {
showWarning(“Can’ t play media - listen to announce!”)
}
func mediaCaching(context: MediaPlayerContext) {
showActivityIndicator()
}
func crewSpeaking(context: MediaPlayerContext) {
set(volume: 100)
}
func safetyVideoPlaying(context: MediaPlayerContext) {
set(volume: 100)
}
func presentHelp(context: MediaPlayerContext) {
showWarning(“Can’ t present help - listen to announce!”)
}
}
Ensuite, créons un contexte avec lequel chaque état fonctionnera et intégrons la machine à états :
final class MediaPlayerContext {
private
var state: State
public fun set(state: State) {
self.state = state
}
public fun play(media: Media) {
state.play(media: media, context: this)
}
…
остальные возможные события
}
Les composants d’application fonctionnent avec le contexte via des méthodes publiques ; les objets d’état décident eux-mêmes de l’état vers lequel effectuer la transition à l’aide de la machine à états à l’intérieur du contexte.
Ainsi, nous avons implémenté la décomposition God Object, maintenir un état changeant sera beaucoup plus facile, grâce au compilateur qui suit les modifications du protocole, réduisant la complexité de compréhension des états en raison de la réduction du nombre de lignes de code, et se concentrant sur résoudre un problème d’État spécifique. Vous pouvez également désormais partager le travail en équipe, en confiant la mise en œuvre d’un état spécifique aux membres de l’équipe, sans vous soucier de la nécessité de « résoudre » les conflits, ce qui se produit lorsque vous travaillez avec une grande classe de machines à états.
Sources
https://refactoring.guru/ru/design-patterns/state