ステートマシンとパターンの条件

この記事では、ステート マシン (State Machine) の使用法について説明し、簡単な実装、State パターンを使用した実装を示します。状態が 3 つ未満の場合、State パターンを使用するのは望ましくないことに注意してください。これにより通常、コードの可読性が不必要に複雑になり、関連するサポートの問題が発生します。すべては適度に行う必要があります。

MEAACT PHOTO / STUART PRICE.

ロードオブフラッグス

民間航空機のメディア システム用のビデオ プレーヤー画面を開発しているとします。プレーヤーは、ビデオ ストリームをロードして再生し、ユーザーがダウンロード プロセスの停止、巻き戻し、その他の通常の操作を実行できる必要があります。選手
です。プレーヤーがビデオ ストリームの次のチャンクをキャッシュし、再生に十分なチャンクがあることを確認し、ユーザーに対してフラグメントの再生を開始し、同時に次のチャンクのダウンロードを継続したとします。
この時点で、ユーザーはビデオの途中まで巻き戻します。つまり、現在のフラグメントの再生を停止し、新しい位置から読み込みを開始する必要があります。ただし、これを実行できない状況もあります。ユーザーは、航空安全に関するビデオが表示されている間、ビデオ ストリームの再生を制御できません。この状況を確認するために isSafetyVideoPlaying フラグを確認してみましょう
。また、システムは現在のビデオを一時停止し、プレーヤーを通じて船長と乗組員からの警告をブロードキャストできなければなりません。別の is NoticePlaying フラグを追加しましょう。さらに、プレーヤーの操作に関するヘルプを表示している間は再生を一時停止しないという要件があります。別のフラグはHelpPresentingです。

メディア プレーヤーの疑似コードの例:

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

上記の例は、変動性 (エントロピー) が大きいため読みにくく、保守も困難です。この例は、ステート マシンを使用していない *多く* のプロジェクトのコード ベースを扱った私の経験に基づいています。
各チェックボックスは、アプリケーションのインターフェイスとビジネス ロジックの要素を明確に「制御」する必要があります。開発者は、別のチェックボックスを追加して、可能なすべてのオプションを使用してすべてを数回確認し、再確認することでそれらを調整できる必要があります。
「2 ^ チェックボックスの数」の式を代入すると、わずか 6 つのチェックボックスに対して 2 ^ 6 = 64 個のアプリケーション動作のオプションが得られます。これらのチェックボックスの組み合わせはすべて手動でチェックして維持する必要があります。
開発者側から見ると、このようなシステムに新しい機能を追加すると次のようになります。
–航空会社のブラウザ ページを表示する機能を追加する必要があります。乗務員が何かをアナウンスする場合、映画と同様に最小限に抑える必要があります。
–わかりました、やります。 (くそー、別のフラグを追加して、フラグが交差するすべての場所を再確認する必要があります。変更する必要があるものがたくさんあります!)

これもフラグ システムの弱点です –アプリケーションの動作に変更を加える。 1 つのフラグだけを変更した後にすべてを再確認する必要がある場合、フラグに基づいて動作を迅速かつ柔軟に変更する方法を想像するのは非常に困難です。この開発アプローチは多くの問題を引き起こし、時間とお金の損失につながります。

マシンに入る

フラグをよく見ると、実際には現実世界で発生する特定のプロセスを処理しようとしていることがわかります。通常モード、安全ビデオの表示、船長や乗組員からのメッセージのブロードキャストなどを列挙します。プロセスごとに、アプリケーションの動作を変更する一連のルールがわかっています。
ステート マシン (ステート マシン) パターンのルールに従って、すべてのプロセスを列挙型の状態としてリストし、プレーヤー コードに状態としての概念を追加し、フラグの組み合わせを削除することで状態ベースの動作を実装します。このようにして、テストのオプションを状態の数と同じ数まで減らします。

疑似コード:

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

フラグ システムとステート マシンの大きな違いは、set(state: ..) メソッドの論理状態切り替えファネルです。これにより、ロジックを実行することなく、人間による状態の理解をプログラム コードに変換できます。コードがさらにサポートされると、フラグを状態に変換するゲーム。

パターンの状態

次に、ステート マシンの単純な実装とステート パターンの違いを示します。 10 個の状態を追加する必要があると想像してください。その結果、ステート マシン クラスはゴッドオブジェクトのサイズまで大きくなり、維持が困難になり、コストが高くなります。もちろん、この実装はフラグ実装よりも優れています (フラグ システムを使用すると、開発者は最初に自分自身を撃ちます。そうでない場合は、2 ^ 10 = 1024 のバリエーションを見て、QA は首を吊るでしょう。ただし、両方とも * 気付かなかった場合) * タスクが複雑な場合、アプリケーションが単純なユーザーは、特定のフラグの組み合わせでの作業を拒否することに気づくでしょう)
州の数が多い場合は州パターンを使用する必要があります。
一連のルールを State プロトコルに追加しましょう。

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

一連のルールの実装を別の状態に移動してみましょう。たとえば、1 つの状態のコードです。

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

次に、各状態が機能するコンテキストを作成し、状態マシンを統合しましょう。

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

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

アプリケーション コンポーネントはパブリック メソッドを通じてコン​​テキストと連携し、コンテキスト内のステート マシンを使用してどの状態からどの状態に遷移するかを決定します。
したがって、God Object 分解を実装しました。プロトコルの変更を追跡するコンパイラーのおかげで、変化する状態の維持がはるかに簡単になり、コード行数の削減により状態を理解する複雑さが軽減され、次の点に焦点が当てられます。特定の状態の問題を解決します。また、1 つの大規模なステート マシン クラスを操作するときに発生する競合を「解決」する必要性を心配することなく、チーム内で作業を共有し、特定の状態の実装をチーム メンバーに提供できるようになりました。

ソース

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