Zustandsmaschine und Musterbedingung

In diesem Artikel werde ich die Verwendung der Zustandsmaschine (State Machine) beschreiben und eine einfache Implementierung zeigen, eine Implementierung unter Verwendung des Zustandsmusters. Es ist erwähnenswert, dass es unerwünscht ist, das Statusmuster zu verwenden, wenn es weniger als drei Zustände gibt, weil Dies führt normalerweise zu einer unnötigen Komplexität der Codelesbarkeit und damit verbundenen Supportproblemen – Alles sollte in Maßen erfolgen.

MEAACT PHOTO / STUART PRICE.

Herr der Flaggen

Angenommen, wir entwickeln einen Videoplayer-Bildschirm für das Mediensystem eines Zivilflugzeugs. Der Player muss in der Lage sein, einen Videostream zu laden und abzuspielen, dem Benutzer das Stoppen des Downloadvorgangs, das Zurückspulen und die Ausführung anderer üblicher Vorgänge zu ermöglichen ein Spieler.
Nehmen wir an, der Player hat den nächsten Teil des Videostreams zwischengespeichert, überprüft, ob genügend Teile für die Wiedergabe vorhanden sind, hat mit der Wiedergabe des Fragments für den Benutzer begonnen und fährt gleichzeitig mit dem Herunterladen des nächsten Teils fort.
In diesem Moment spult der Benutzer zur Mitte des Videos zurück, d. h. Sie müssen jetzt die Wiedergabe des aktuellen Fragments stoppen und an einer neuen Position mit dem Laden beginnen. Es gibt jedoch Situationen, in denen dies nicht möglich ist – Der Benutzer kann die Wiedergabe des Videostreams nicht steuern, während ihm ein Video über Flugsicherheit gezeigt wird. Lassen Sie uns das isSafetyVideoPlaying-Flag überprüfen, um diese Situation zu überprüfen.
Das System muss außerdem in der Lage sein, das aktuelle Video anzuhalten und über den Player eine Warnung des Schiffskapitäns und der Schiffsbesatzung zu senden. Fügen wir ein weiteres isAnnouncementPlaying-Flag hinzu. Außerdem besteht die Anforderung, die Wiedergabe nicht anzuhalten, während Hilfe zur Arbeit mit dem Player angezeigt wird. Ein weiteres Flag ist HelpPresenting.

Beispiel-Pseudocode für einen Mediaplayer:

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

Das obige Beispiel ist aufgrund der hohen Variabilität (Entropie) schwer zu lesen und schwer zu warten. Dieses Beispiel basiert auf meiner Erfahrung mit der Codebasis *vieler* Projekte, die keine Zustandsmaschine verwendeten.
Jedes Kontrollkästchen muss speziell die Elemente der Schnittstelle und der Geschäftslogik der Anwendung „steuern“. Durch das Hinzufügen eines weiteren Kontrollkästchens muss der Entwickler in der Lage sein, sie zu jonglieren und alles mehrmals mit allen möglichen Optionen zu überprüfen.
Durch Einsetzen in die Formel „2 ^ Anzahl der Kontrollkästchen“ erhalten Sie 2 ^ 6 = 64 Optionen für das Anwendungsverhalten für nur 6 Kontrollkästchen. Alle diese Kombinationen von Kontrollkästchen müssen manuell überprüft und verwaltet werden.
Aus Sicht des Entwicklers sieht das Hinzufügen neuer Funktionen mit einem solchen System folgendermaßen aus:
– Wir müssen die Möglichkeit hinzufügen, die Browserseite der Fluggesellschaft anzuzeigen, und sie sollte wie bei Filmen minimiert werden, wenn Besatzungsmitglieder etwas ankündigen.
– Ok, ich werde es tun. (Oh verdammt, ich muss eine weitere Flagge hinzufügen und alle Stellen, an denen sich die Flaggen kreuzen, noch einmal überprüfen, das sind viele Dinge, die geändert werden müssen!)

Auch ein Schwachpunkt des Flaggensystems – Änderungen am Verhalten der Anwendung vornehmen. Es ist sehr schwer vorstellbar, wie man das Verhalten anhand von Flags schnell/flexibel ändern kann, wenn man nach der Änderung nur eines Flags alles noch einmal überprüfen muss. Dieser Entwicklungsansatz führt zu vielen Problemen, Zeit- und Geldverlusten.

Betreten Sie die Maschine

Wenn Sie sich die Flags genau ansehen, können Sie verstehen, dass wir tatsächlich versuchen, bestimmte Prozesse zu verarbeiten, die in der realen Welt auftreten. Wir listen sie auf: Normalmodus, Anzeige eines Sicherheitsvideos, Übertragung einer Nachricht des Kapitäns oder der Besatzungsmitglieder. Für jeden Prozess ist ein Regelwerk bekannt, das das Verhalten der Anwendung verändert.
Gemäß den Regeln des State-Machine-Musters (State-Machine) werden wir alle Prozesse als Zustände in der Aufzählung auflisten, ein solches Konzept als Zustand zum Player-Code hinzufügen und zustandsbasiertes Verhalten implementieren, indem wir Kombinationen auf den Flags entfernen. Dadurch reduzieren wir die Testmöglichkeiten auf genau die Anzahl der Zustände.

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

Der große Unterschied zwischen einem Flag-System und einer Zustandsmaschine ist der logische Zustandsschalttrichter in der set(state: ..)-Methode, der es Ihnen ermöglicht, das menschliche Verständnis des Zustands in Programmcode zu übersetzen, ohne sich mit Logik auseinandersetzen zu müssen Spiele zum Konvertieren von Flags in Zustände bei weiterer Codeunterstützung.

Musterstatus

Als nächstes werde ich den Unterschied zwischen der naiven Implementierung der Zustandsmaschine und dem Zustandsmuster zeigen. Stellen wir uns vor, wir müssten 10 Zustände hinzufügen. Dadurch wächst die Zustandsmaschinenklasse auf die Größe eines Gottobjekts, dessen Wartung schwierig und kostspielig sein wird. Natürlich ist diese Implementierung besser als die Flag-Implementierung (mit dem Flag-System erschießt sich der Entwickler zuerst selbst, und wenn nicht, dann hängt sich die Qualitätssicherung bei 2 ^ 10 = 1024 Variationen auf, aber wenn beide *nicht tun Beachten* Sie die Komplexität der Aufgabe, dann wird der Benutzer, dessen Anwendung einfach ist, bemerken, dass sie sich weigert, mit einer bestimmten Kombination von Flags zu arbeiten)
Bei einer großen Anzahl von Zuständen ist die Verwendung des Zustandsmusters erforderlich.
Fügen wir dem State-Protokoll eine Reihe von Regeln hinzu:

protocol State {
    func playMedia(media: Media, context: MediaPlayerContext)
    func shouldCacheMedia(context: MediaPlayerContext)
    func crewSpeaking(context: MediaPlayerContext)
    func safetyVideoPlaying(context:MediaPlayerContext)
    func presentHelp(context: MediaPlayerContext)
}

Lassen Sie uns die Implementierung des Regelsatzes in separate Zustände verschieben, zum Beispiel den Code für einen Zustand:

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

Als nächstes erstellen wir einen Kontext, mit dem jeder Zustand arbeiten wird, und integrieren die Zustandsmaschine:

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

	…
	остальные возможные события
}

Anwendungskomponenten arbeiten mit dem Kontext über öffentliche Methoden; Zustandsobjekte entscheiden selbst, von welchem ​​Zustand aus sie über die Zustandsmaschine innerhalb des Kontexts wechseln.
Daher haben wir die God-Object-Zerlegung implementiert. Die Aufrechterhaltung eines sich ändernden Zustands wird viel einfacher, da der Compiler Änderungen im Protokoll verfolgt und die Komplexität des Verständnisses von Zuständen aufgrund der Reduzierung der Anzahl der Codezeilen verringert und sich darauf konzentriert Lösung eines bestimmten Staatsproblems. Sie können jetzt auch die Arbeit in einem Team teilen und Teammitgliedern die Implementierung eines bestimmten Zustands übertragen, ohne sich Gedanken über die Notwendigkeit machen zu müssen, Konflikte zu „lösen“, was bei der Arbeit mit einer großen Zustandsmaschinenklasse der Fall ist.

Quellen

https://refactoring.guru/ru/design-patterns/state