コンピュータ ウィンドウズ インターネット

州。 各州の州クラス

行動デザインパターン。 これは、プログラムの実行中に、オブジェクトの状態に応じて動作を変更する必要がある場合に使用されます。 古典的な実装には、すべてのメソッドと、考えられる状態ごとに 1 つのクラスを含む基本抽象クラスまたはインターフェイスの作成が含まれます。 このパターンは、「条件ステートメントをポリモーフィズムで置き換える」推奨事項の特殊なケースです。

すべてが本に従っているように見えますが、ニュアンスがあります。 特定の状態に関係のないメソッドを正しく実装するにはどうすればよいでしょうか? たとえば、空のカートから商品を削除したり、空のカートの代金を支払うにはどうすればよいでしょうか? 通常、各状態クラスは関連するメソッドのみを実装し、それ以外の場合は InvalidOperationException をスローします。

人の代わりにリスコフを置くという原則の違反。 Yaron Minsky 氏は別のアプローチを提案しました。 不法国家を代表不能にする。 これにより、エラー チェックを実行時からコンパイル時に移行できるようになります。 ただし、この場合の制御フローは、ポリモーフィズムを使用せず、パターン マッチングに基づいて編成されます。 幸いなことに、 。

F# トピックの例の詳細 不法国家を代表不能にするスコット・ヴラシン氏のウェブサイトで明らかにした。

バスケットを例に「状態」の実装を考えてみましょう。 C# には組み込みの共用体型がありません。 データと動作を分離しましょう。 enum を使用して状態自体をエンコードし、動作を別のクラスとしてエンコードします。 便宜上、列挙型と対応する動作クラスである基本「状態」クラスを接続する属性を宣言し、列挙型から動作クラスに移動する拡張メソッドを追加しましょう。

インフラストラクチャー

public class StateAttribute: Attribute ( public Type StateType ( get; ) public StateAttribute(Type stateType) ( StateType = stateType ?? throw new ArgumentNullException(nameof(stateType)); ) ) public abstract class State ここで、 T: class ( protected State(T エンティティ) ( Entity = エンティティ ?? throw new ArgumentNullException(nameof(entity)); ) protected T Entity ( get; ) ) public static class StateCodeExtensions ( public static State 状態まで (この Enum stateCode、オブジェクト エンティティ) where T: class // はい、はいリフレクションが遅い。 コンパイル済みの式ツリー // または IL Emit に置き換えると高速になります => (状態 ) Activator.CreateInstance(stateCode .GetType() .GetCustomAttribute ().StateType、エンティティ); )

対象分野

「カート」エンティティを宣言しましょう。

パブリックインターフェイス IHasState ここで、TEntity: クラス ( TStateCode StateCode ( get; ) 状態 State ( get; ) ) パブリック部分クラス Cart: IHasState ( public User User ( get; protected set; ) public CartStateCode StateCode ( get; protected set; ) public State 状態 => StateCode.ToState (これ); public 10mal Total ( get; protected set; ) protected virtual ICollection Products (get; set; ) = 新しいリスト (); // ORM のみ protected Cart() ( ) public Cart(User user) ( User = user ?? throw new ArgumentNullException(nameof(user)); StateCode = StateCode = CartStateCode.Empty; ) public Cart(User user, IEnumerable Products) : this(user) ( StateCode = StateCode = CartStateCode.Empty; foreach (var product in products) ( Products.Add(product); ) ) public Cart(User user, IEnumerable 製品、10 進合計) : this(ユーザー、製品) ( if (合計<= 0) { throw new ArgumentException(nameof(total)); } Total = total; } }
カートの状態 (空、アクティブ、支払い済み) ごとに 1 つのクラスを実装しますが、共通のインターフェイスは宣言しません。 各状態が関連する動作のみを実装するようにします。 これは、EmptyCartState、ActiveCartState、PaidCartState クラスがすべて同じインターフェイスを実装できないという意味ではありません。 使用できますが、そのようなインターフェイスには各状態で使用可能なメソッドのみを含める必要があります。 この例では、Add メソッドは EmptyCartState と ActiveCartState で使用できるため、抽象 AddableCartStateBase からそれらを継承できます。 ただし、アイテムを追加できるのは未払いのカートのみであるため、すべての州に共通のインターフェイスはありません。 このようにして、コンパイル時にコード内に InvalidOperationException がないことが保証されます。

パブリック部分クラス Cart ( public enum CartStateCode: byte ( Empty, Active, Paid ) public Interface IAddableCartState ( ActiveCartState Add(Product product); IEnumerable Products ( get; ) ) パブリック インターフェイス INotEmptyCartState ( IEnumerable Products ( get; ) 10 進数 Total ( get; ) ) パブリック抽象クラス AddableCartState: State , IAddableCartState ( protected AddableCartState(Cart エンティティ):base(entity) ( ) public ActiveCartState Add(Product product) ( Entity.Products.Add(product); Entity.StateCode = CartStateCode.Active; return (ActiveCartState)Entity.State; )パブリック IEnumerable 製品 => Entity.Products; ) パブリック クラス EmptyCartState: AddableCartState ( public EmptyCartState(カート エンティティ): ベース(エンティティ) ( ) ) パブリック クラス ActiveCartState: AddableCartState, INotEmptyCartState ( パブリック ActiveCartState(カート エンティティ): ベース(エンティティ) ( ) パブリック PaidCartState Pay(10 進合計) ( Entity.Total = total; Entity.StateCode = CartStateCode.Paid; return (PaidCartState)Entity.State; ) public State Remove(Product product) ( Entity.Products.Remove(product); if(!Entity.Products.Any()) ( Entity.StateCode = CartStateCode.Empty; ) return Entity.State; ) public EmptyCartState Clear() ( Entity. Products.Clear(); Entity.StateCode = CartStateCode.Empty; return (EmptyCartState)Entity.State; ) public 10mal Total => Products.Sum(x => x.Price); ) パブリック クラス PaidCartState: 状態 、INotEmptyCartState (パブリック IEnumerable 製品 => Entity.Products; パブリック 10 進数 Total => Entity.Total; public PaidCartState(カート エンティティ) : ベース(エンティティ) ( ) ) )
状態は入れ子になっていると宣言されます ( 入れ子になった) クラスは偶然ではありません。 ネストされたクラスは、Cart クラスの保護されたメンバーにアクセスできます。つまり、動作を実装するためにエンティティのカプセル化を犠牲にする必要はありません。 エンティティ クラス ファイルが乱雑にならないように、partial キーワードを使用して宣言を Cart.cs と CartStates.cs の 2 つに分割しました。

Public ActionResult GetViewResult(State cartState) ( switch (cartState) ( case Cart.ActiveCartState activeState: return View("Active", activeState); case Cart.EmptyCartState emptyState: return View("Empty", emptyState); case Cart.PaidCartStatepaidCartState: return View(" Paid",paidCartState); デフォルト: throw new InvalidOperationException(); ) )
カートの状態に応じて、異なるビューを使用します。 カートが空の場合は、「カートは空です」というメッセージが表示されます。 アクティブなカートには、製品のリスト、製品数の変更および一部の製品の削除機能、「注文する」ボタン、および合計購入金額が含まれます。

有料カートはアクティブなカートと同じように見えますが、何も編集することはできません。 この事実は、INotEmptyCartState インターフェイスを強調表示することでわかります。 したがって、リスコフ置換原則の違反を取り除くだけでなく、界面分離の原則も適用しました。

結論

アプリケーション コードでは、IAddableCartState および INotEmptyCartState インターフェイス リンクを操作して、カートに商品を追加し、カート内の商品を表示するコードを再利用できます。 パターン マッチングは、型間に共通点がない場合にのみ C# の制御フローに適していると思います。 他の場合には、ベースリンクを使用した方が便利です。 同様の手法は、エンティティの動作をエンコードするだけでなく、エンティティに対しても使用できます。

告白する時が来ました。私はこのメインの話で少しやりすぎました。 GoF State のデザイン パターンに関するものであるはずでした。 しかし、ゲームでの使用については、コンセプトに触れずに語ることはできません。 有限状態マシン(または「FSM」)。 でも一度入ってみると、覚えておかなければいけないことに気づきました 階層型ステートマシンまたは 階層型オートマトンそして マガジンメモリ付き自動機(プッシュダウンオートマトン).

これは非常に広範なトピックなので、この章をできるだけ短くするために、いくつかの明らかなコード例は省略し、ギャップの一部を自分で埋める必要があります。 これによって理解が難しくならないことを願っています。

有限状態マシンについて聞いたことがなくても動揺する必要はありません。 これらは AI 開発者やコンピューターハッカーにはよく知られていますが、他の分野ではほとんど知られていません。 私の考えでは、それらはもっと評価されるべきだと思うので、それらが解決する問題のいくつかを紹介したいと思います。

これらはすべて、人工知能の古い初期の頃のエコーです。 50 年代から 60 年代にかけて、人工知能は主に言語構造の処理に焦点を当てていました。 最新のコンパイラで使用されているテクノロジの多くは、人間の言語を解析するために発明されました。

私たちは皆そこに行ったことがある

小さな横スクロールのプラットフォーマーを開発しているとします。 私たちの仕事は、ゲーム世界でプレイヤーのアバターとなるヒロインをモデル化することです。 これは、ユーザー入力に応答する必要があることを意味します。 Bを押すとジャンプします。 非常に単純です:

void Heroine::handleInput(入力入力) ( if (input == PRESS_B) ( yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) )

バグに気づきましたか?

ここには「空中ジャンプ」を防ぐコードはありません。 彼女が空中にいる間にBを押し続けると、彼女は何度も飛び上がります。 これを解決する最も簡単な方法は、ヒロインがいつジャンプしたかを追跡するブール値フラグ isJumping_ を Heroine に追加することです。

void Heroine::handleInput(Input input) ( if (input == PRESS_B) ( if (!isJumping_) ( isJumping_ = true ; // Jump... ) ) )

ヒロインが再び地面に触れたときに isJumping_ を false に戻すコードも必要です。 わかりやすくするために、このコードは省略しています。

void Heroine::handleInput(入力入力) ( if (入力 == PRESS_B) ( // まだジャンプしていない場合はジャンプしましょう...) else if (input == PRESS_DOWN) ( if (!isJumping_) ( setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( setGraphics(IMAGE_STAND); ) )

ここでバグに気づきましたか?

このコードを使用すると、プレーヤーは次のことができます。

  1. 押し下げてスクワットします。
  2. B を押すと座った状態からジャンプします。
  3. 空中で下に放します。

同時にヒロインが空中に立つグラフィックに切り替わります。 別のフラグを追加する必要があります...

void Heroine::handleInput(Input input) ( if (input == PRESS_B) ( if (!isJumping_ && !isDucking_) ( // Jump... ) ) else if (input == PRESS_DOWN) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( isDucking_ = false ; setGraphics(IMAGE_STAND); ) )

ここで、プレイヤーが空中で押し込んだときにヒロインがタックルで攻撃できる機能を追加するとよいでしょう。

void Heroine::handleInput(Input input) ( if (input == PRESS_B) ( if (!isJumping_ && !isDucking_) ( // Jump... ) ) else if (input == PRESS_DOWN) ( if (!isJumping_) ( isDucking_ = true ; setGraphics(IMAGE_DUCK); ) else ( isJumping_ = false ; setGraphics(IMAGE_DIVE); ) ) else if (input == RELEASE_DOWN) ( if (isDucking_) ( // 立っている... ) )

再びバグを探します。 それを見つけた?

空中ジャンプを不可能にするチェックはありますが、タックル中は不可能です。 別のフラグを追加しています...

このアプローチには何か問題があります。 コードに触れるたびに、何かが壊れます。 もっと動きを追加する必要がありますが、それすらありません 歩くいいえ、しかしこのアプローチでは多くのバグを克服する必要があります。

私たち皆が理想化し、素晴らしいコードを作成するプログラマーは、実際にはスーパーマンではありません。 彼らは、エラーが発生する恐れのあるコードに対する本能を発達させ、可能な限りエラーを回避しようとしているだけです。

複雑な分岐や状態の変化は、まさに避けるべきタイプのコードです。

有限状態マシンは私たちの救いです

イライラしたあなたは、机から鉛筆と紙以外のものをすべて取り除き、フローチャートを描き始めます。 ヒロインが実行できるアクション (立つ、ジャンプする、しゃがむ、転がる) ごとに長方形を描画します。 どの状態でもキーの押下に応答できるように、これらの四角形の間に矢印を描き、その上にボタンのラベルを付けて、状態を接続します。

おめでとうございます。作成されました ステートマシン (有限状態マシン)。 それらは、と呼ばれるコンピューターサイエンスの分野から来ています。 オートマトン理論 (オートマトン理論)、その構造群には有名なチューリング マシンも含まれています。 FSM は、このファミリーの最も単純なメンバーです。

結論は次のとおりです。

    固定セットがあります 、機関銃が含まれている可能性があります。この例では、立ったり、ジャンプしたり、しゃがんだり、転がったりします。

    マシンは次の場所にのみ存在できます。 1ついつでも状態。私たちのヒロインは、ジャンプすることと立つことを同時に行うことができません。 実はこれを防ぐためにそもそもFSMが使われているのです。

    後続 入力または イベント、マシンに送信されます。この例では、これはボタンを押したり放したりすることです。

    それぞれの州には、 トランジションセット、それぞれが入力に関連付けられており、状態を示します。ユーザー入力が発生すると、それが現在の状態と一致する場合、マシンは矢印が指す場所に状態を変更します。

    例えば、立った状態で押し込むとしゃがんだ状態に移行します。 ジャンプ中に下を押すとタックル状態に変化します。 現在の状態で入力に遷移が提供されていない場合は、何も起こりません。

最も純粋な形では、これは状態、入力、遷移というバナナ全体です。 それらをブロック図の形式で表すことができます。 残念ながら、コンパイラはそのような走り書きを理解できません。 それでどうやって 埋め込む有限状態マシン? Gang of Four は独自のバージョンを提供していますが、より単純なバージョンから始めます。

私のお気に入りの FSM の例えは、古いテキストクエストの Zork です。 通路でつながった部屋で構成される世界があります。 「北に行く」などのコマンドを入力すると、それらを探索できます。

このようなマップは、有限状態マシンの定義に完全に対応します。 あなたがいる部屋は現在の状態です。 部屋から出るたびにトランジションが発生します。 ナビゲーション コマンド - 入力。

列挙とスイッチ

古い Heroine クラスの問題の 1 つは、ブール キーの誤った組み合わせ ( isJumping_ と isDucking_ ) が許可されていることです。これらは同時に true になることはできません。 また、複数のブール値フラグがあり、そのうちの 1 つだけが true になれる場合は、それらをすべて enum に置き換えた方がよいのではないでしょうか。

私たちの場合、enum を使用して、次のように FSM のすべての状態を完全に記述することができます。

enum 状態 ( STATE_STANDING、STATE_JUMPING、STATE_DUCKING、STATE_DIVING );

Heroine には、多数のフラグの代わりに、state_ フィールドが 1 つだけあります。 分岐順序も変更する必要があります。 前のコード例では、最初に入力に応じて分岐し、次に状態に応じて分岐しました。 その際、押されたボタンごとにコードをグループ化しましたが、状態に関連付けられたコードはぼやけました。 今度はその逆を行い、状態に応じて入力を切り替えます。 得られる結果は次のとおりです。

void Heroine::handleInput(Input input) ( switch (state_) ( case STATE_STANDING: if (input == PRESS_B) ( state_ = STATE_JUMPING; yVelocity_ = JUMP_VELOCITY; setGraphics(IMAGE_JUMP); ) else if (input == PRESS_DOWN) ( state_ = STATE_DUCKING; setGraphics(IMAGE_DUCK); ) ブレーク ; case STATE_JUMPING: if (input == PRESS_DOWN) ( state_ = STATE_DIVING; setGraphics(IMAGE_DIVE); ) ブレーク ; case STATE_DUCKING: if (input == RELEASE_DOWN) ( state_ = STATE_STANDING; setGraphics (IMAGE_STAND); ) ブレーク ; ) )

非常に簡単に見えますが、それでも、このコードはすでに前のコードよりもはるかに優れています。 条件分岐はまだいくつかありますが、可変状態を 1 つのフィールドに単純化しました。 単一の状態を管理するすべてのコードが 1 か所に集められています。 これは有限状態マシンを実装する最も簡単な方法であり、場合によってはこれで十分です。

これでヒロインはもう参加できなくなります 不確かな状態。 ブール フラグを使用する場合、いくつかの組み合わせは可能ですが、意味がありませんでした。 enum を使用する場合、すべての値が正しいです。

残念ながら、問題がこの解決策を超えて大きくなる可能性があります。 ヒロインに特別な攻撃を追加したいとします。そのためには、ヒロインは座って充電し、蓄積されたエネルギーを放出する必要があります。 座っている間は充電時間に注意する必要があります。

充電時間を保存するために、chargeTime_ フィールドを Heroine に追加します。 すべてのフレームで呼び出される update() メソッドがすでにあるとします。 次のコードを追加してみましょう。

void Heroine::update() ( if (state_ == STATE_DUCKING) (chargeTime_++; if (chargeTime_ > MAX_CHARGE) ( superBomb(); ) ) )

更新方法のパターンを推測すると、賞品を獲得できます。

再びスクワットするたびに、このタイマーをリセットする必要があります。 これを行うには、 handleInput() を変更する必要があります。

void Heroine::handleInput(Input input) ( switch (state_) ( case STATE_STANDING: if (input == PRESS_DOWN) ( state_ = STATE_DUCKING;chargeTime_ = 0 ; setGraphics(IMAGE_DUCK); ) // 残りの入力を処理します...壊す ; // 他の状態... } }

結局、このチャージ攻撃を追加するには、しゃがみ状態でのみ使用されるにもかかわらず、2つのメソッドを変更し、HeroineにchargeTime_フィールドを追加する必要がありました。 このコードとデータをすべて 1 か所にまとめたいと考えています。 Gang of Four がこれを手伝ってくれます。

テンプレートの状態

オブジェクト指向パラダイムに精通している人にとって、すべての条件分岐は動的ディスパッチ (つまり、C++ の仮想メソッドの呼び出し) を使用する機会となります。 このウサギの穴をさらに深く掘り下げる必要があると思います。 必要なのはそれだけである場合もあります。

これには歴史的根拠があります。 Gang of Four やそのグループなど、オブジェクト指向パラダイムの古い使徒の多くは、 プログラミングパターンマーティン・フラーと彼の リファクタリング Smalltalk から来ました。 そして、ifThen は条件を処理するために使用する単なるメソッドであり、true オブジェクトと false オブジェクトに対して異なる方法で実装されます。

この例では、オブジェクト指向に注意を払う必要がある臨界点にすでに到達しています。 これにより、State パターンがわかります。 ギャング・オブ・フォーの言葉を引用すると:

内部状態の変化に応じてオブジェクトの動作を変更できるようにします。 この場合、オブジェクトは別のクラスのように動作します。

あまり明確ではありません。 結局、switchもこれに対応しました。 ヒロインの例に関連して、テンプレートは次のようになります。

ステータスインターフェース

まず、状態のインターフェイスを定義しましょう。 状態に依存する動作の各ビット、つまり 以前に switch を使用して実装したものはすべて、このインターフェイスの仮想メソッドに変わります。 私たちの場合、これらは handleInput() と update() です。

class HeroineState ( public : virtual ~HeroineState() () 仮想ボイドハンドル入力{} {} };

各州のクラス

状態ごとに、インターフェイスを実装するクラスを定義します。 彼の方法は、この状態でのヒロインの行動を決定します。 言い換えれば、前の例のスイッチからすべてのオプションを取得し、それらを状態クラスに変換します。 例えば:

class DuckingState: public HeroineState ( public : DuckingState() :chargeTime_(0 ) () 仮想ボイドハンドル入力 (ヒロイン&ヒロイン、入力入力)( if (入力 == RELEASE_DOWN) ( // スタンディング状態に遷移...ヒロイン.setGraphics(IMAGE_STAND); )) バーチャルボイドアップデート(ヒロイン&ヒロイン)(chargeTime_++; if (chargeTime_ > MAX_CHARGE) (heroine.superBomb(); ) ) private : int ChargeTime_; );

ChargeTime_ をヒロイン自身のクラスから DuckingState クラスに移動したことに注意してください。 このデータはこの状態でのみ意味を持ち、データ モデルはこれを明確に示しているため、これは非常に良いことです。

州への代表団

クラスヒロイン(公開: virtual void handleInput(入力入力)( state_->handleInput(*this , input); ) 仮想ボイド更新()( state_->update(*this ); ) // 他のメソッド...プライベート : HeroineState* state_; );

「状態を変更」するには、state_ が別の HeroineState オブジェクトを指すようにするだけです。 これが State パターンの実際の構成要素です。

GoF の Strategy および Type Object テンプレートに非常に似ています。 3 つすべてで、スレーブに委任するマスター オブジェクトがあります。 違いは 目的.

  • 戦略の目的は、 接続性の低下メインクラスとその動作を(分離)します。
  • Type オブジェクトの目的は、共通の Type オブジェクトを共有することによって、同じように動作する多数のオブジェクトを作成することです。
  • State の目的は、委任先のオブジェクトを変更することで、メイン オブジェクトの動作を変更することです。

これらの状態オブジェクトはどこにあるのでしょうか?

あなたに言っていなかったことがあります。 状態を変更するには、state_ に新しい状態を指す新しい値を割り当てる必要がありますが、このオブジェクトはどこから来たのでしょうか? enum の例では、何も考える必要はありません。enum 値は数値のような単なるプリミティブです。 しかし、現在では状態はクラスによって表されるため、実際のインスタンスへのポインターが必要になります。 最も一般的な答えは 2 つあります。

静的状態

状態オブジェクトに他のフィールドがない場合、状態オブジェクトに格納されるのは、メソッドの内部仮想テーブルへのポインタだけであり、これらのメソッドを呼び出すことができます。 この場合、クラスのインスタンスを複数持つ必要はありません。各インスタンスは同じままです。

状態にフィールドがなく、仮想メソッドが 1 つだけある場合は、パターンをさらに単純化できます。 それぞれ交換していきます クラス関数 state - 通常のトップレベル関数。 そしてそれに応じてフィールド 州_メインクラスの は単純な関数ポインタに変わります。

1つだけでも十分に対応可能です 静的コピー。 同時に同じ状態にある大量の FSM がある場合でも、ステート マシン固有のものは何もないため、それらはすべて同じ静的インスタンスを指すことができます。

静的インスタンスをどこに配置するかはあなた次第です。 適切な場所を見つけてください。 インスタンスを基本クラスに入れてみましょう。 理由もなく。

class HeroineState ( public : 静的 StandingState スタンディング; 静的 DuckingState ダッキング; 静的 JumpingState ジャンプ; 静的 DivingState ダイビング; // コードの残りの部分... };

これらの静的フィールドのそれぞれは、ゲームで使用される状態のインスタンスです。 ヒロインをジャンプさせるには、立っている状態で次のようなことを行います。

if (input == PRESS_B) (heroine.state_ = &HeroineState::jumping; hero.setGraphics(IMAGE_JUMP); )

状態インスタンス

場合によっては、前のオプションがうまくいかないことがあります。 静止状態はしゃがんだ状態には適していません。 ChargeTime_ フィールドがあり、しゃがむヒロインに固有のものです。 私たちの場合、ヒロインが 1 人しかいないため、これはさらにうまく機能しますが、2 人のプレイヤーの協力プレイを追加したい場合は、大きな問題が発生します。

この場合、状態オブジェクトに移動するときに、状態オブジェクトを作成する必要があります。 これにより、各 FSM が独自の状態インスタンスを持つことができるようになります。 もちろん、メモリを割り当てると、 新しい条件、これは私たちがすべきであることを意味します リリース現在のメモリが占​​有されています。 変更を引き起こすコードはメソッドの現在の状態にあるため、注意が必要です。 私たちはこれを私たち自身の下から取り除きたくありません。

代わりに、HeroineState の handleInput() がオプションで新しい状態を返すようにします。 これが起こると、Heroine は次のように古い状態を削除し、新しい状態に置き換えます。

void Heroine::handleInput(Input input) ( HeroineState* state = state_->handleInput(*this , input); if (state != NULL ) ( delete state_; state_ = state; ) )

こうすることで、メソッドから戻るまで前の状態は削除されません。 ここで、新しいインスタンスを作成することで、スタンディング状態からダイビング状態に移行できます。

HeroineState* StandingState::handleInput(Heroine& ヒロイン, Input input) ( if (input == PRESS_DOWN) ( // その他のコード... return new DuckingState(); ) // この状態を維持します。 return NULL ; )

可能であれば、静的状態を使用することを好みます。状態が変化するたびにオブジェクトを割り当てることでメモリと CPU サイクルを消費しないからです。 以上ではない条件については、 - これはまさにあなたが必要としているものです。

もちろん、状態にメモリを動的に割り当てる場合は、メモリの断片化の可能性について考慮する必要があります。 オブジェクト プール テンプレートが役に立ちます。

ログインとログアウトの手順

State パターンは、すべての動作と関連データを 1 つのクラス内にカプセル化するように設計されています。 かなり順調に進んでいますが、まだ詳細が不明な点がいくつかあります。

ヒロインの状態が変わると、スプライトも切り替わります。 現時点では、このコードは州に属しており、 彼女は切り替えます。 状態がダイビングからスタンディングに移行すると、ダイビングはそのイメージを確立します。

HeroineState* DuckingState::handleInput(ヒロイン&ヒロイン, 入力入力) ( if (input == RELEASE_DOWN) ( Heroine.setGraphics(IMAGE_STAND); return newstandingState(); ) // その他のコード... )

私たちが本当に望んでいるのは、各州が独自のグラフィックスを制御することです。 状態に追加することでこれを実現できます 入力アクション (エントリーアクション):

クラス立っている状態: public HeroineState ( public : バーチャルボイドエンター(ヒロイン&ヒロイン)( Heroine.setGraphics(IMAGE_STAND); ) // 他のコード... );

Heroine に戻り、コードを変更して、状態の変更が新しい状態の入力アクション関数の呼び出しを伴うようにします。

void Heroine::handleInput(Input input) ( HeroineState* state = state_->handleInput(*this , input); if (state != NULL ) ( delete state_; state_ = state; // 新しいステートの入力アクションを呼び出します。 state_->enter(*this ); ))

これにより、DuckingState コードが簡素化されます。

HeroineState* DuckingState::handleInput(ヒロイン&ヒロイン, 入力入力) ( if (input == RELEASE_DOWN) ( return newstandingState(); ) // その他のコード... )

これはスタンディングに切り替えるだけで、グラフィックスはスタンディング状態で処理されます。 これで、私たちの状態は真にカプセル化されました。 このような入力アクションのもう 1 つの優れた特徴は、状態に関係なく、その状態に入るとトリガーされることです。 どれの僕等がいた。

実際の状態グラフのほとんどには、同じ状態への複数の遷移があります。 たとえば、ヒロインは立ったり、座ったり、ジャンプしたりしながら武器を撃つことができます。 これは、これが発生する場合は常にコードの重複が発生する可能性があることを意味します。 入力アクションを使用すると、それを 1 か所に集めることができます。

類推すればできるよ 出力アクション (終了アクション)。 これは単に、前に状態に対して呼び出すメソッドになります。 出発する新しい状態に切り替えます。

そして私たちは何を達成したのでしょうか?

私はあなたに FSM を売り込むのに非常に多くの時間を費やしてきましたが、今、あなたの下から敷物を引き抜こうとしています。 これまで私が述べてきたことはすべて真実であり、優れた問題解決策となります。 しかし、偶然にも、有限状態マシンの最も重要な利点が最大の欠点でもあるのです。

ステート マシンは、コードを非常に厳密な構造に編成することで、コードのもつれを真剣に解きほぐすのに役立ちます。 私たちが持っているのは、固定された状態のセット、単一の現在の状態、およびハードコードされた遷移だけです。

有限オートマトンはチューリング完全ではありません。 オートマトン理論は、それぞれが最後のモデルよりも複雑な一連の抽象モデルを通じて完全性を説明します。 チューリング マシンは最も表現力豊かなマシンの 1 つです。

「完全なチューリング」とは、チューリング マシンを実装するのに十分な表現力を持つシステム (通常はプログラミング言語) を意味します。 これは、すべてのチューリング完全言語がほぼ同等に表現力があることを意味します。 FSM はこのクラブに参加するには十分な表現力を持っていません。

ゲーム AI など、より複雑なものにステート マシンを使用しようとすると、すぐにこのモデルの制限に遭遇します。 幸いなことに、私たちの先人たちはいくつかの障害を回避する方法を学びました。 そのような例をいくつか挙げてこの章を終わります。

競争状態マシン

私たちはヒロインに武器を運ぶ能力を追加することにしました。 彼女は現在武装していますが、走る、ジャンプする、しゃがむなど、以前にできたすべてのことをまだ行うことができます。 しかし今では、これらすべてを行いながら、武器を発砲することもできます。

この動作を FSM フレームワークに適合させたい場合は、状態の数を 2 倍にする必要があります。 それぞれの状態に対して、同じものをもう 1 つ作成する必要がありますが、武器を持ったヒロインの場合は、立っている、武器を持って立っている、ジャンプしている、武器を持ってジャンプしている...まあ、アイデアはわかります。

さらにいくつかの武器を追加すると、組み合わせにより状態の数が増加します。 そして、これは単なる国家の集合体ではなく、繰り返しの集合体でもあります。武装国家と非武装国家は、銃撃を担当するコードの部分を除けば、ほぼ同一です。

ここでの問題は、状態の 2 つの部分を混同していることです。 するだから何 手に持つ- 1台のマシンで。 考えられるすべての組み合わせをモデル化するには、それぞれの状態を作成する必要があります。 カップル。 解決策は明らかです。2 つの別々のステート マシンを作成する必要があります。

私たちが団結したいなら n動作状態と メートル私たちが手に持っているものの状態を 1 つの有限状態マシンに変換する - 私たちが必要とするのは n×m州。 機関銃が 2 丁ある場合、次のものが必要になります。 n+m州。

最初のステートマシンのアクションは変更せずにそのままにしておきます。 それに加えて、ヒロインが何を持っているかを説明する別のマシンを作成します。 これで、Heroine は各マシンに 1 つずつ、合計 2 つの「状態」参照を持つことになります。

クラスヒロイン( // コードの残りの部分...プライベート : HeroineState* state_; HeroineState* 装備_; );

説明のために、2 番目のステート マシンの State パターンの完全な実装を使用しますが、実際には、この場合は単純なブール フラグで十分です。

ヒロインが入力をステートに委任すると、その翻訳が両方のステート マシンに渡されます。

void Heroine::handleInput(入力入力) ( state_->handleInput(*this , input); Equipment_->handleInput(*this , input); )

より複雑なシステムには、入力の一部を吸収して他のマシンが入力を受信できなくなる有限状態マシンが含まれる場合があります。 これにより、複数のマシンが同じ入力に応答する状況を防ぐことができます。

各ステート マシンは、他のステート マシンとは独立して入力に反応し、動作を生成し、その状態を変更できます。 そして、両方の状態が実質的に無関係である場合、それはうまく機能します。

実際には、状態が相互に作用する状況に遭遇する可能性があります。 たとえば、ジャンプ中に射撃したり、武装しているときにスライド攻撃を実行したりすることはできません。 この動作とコード内のオートマトンの調整を確実にするには、 if 経由で同じ総当たりチェックに戻る必要があります。 別の有限状態マシン。 最もエレガントなソリューションではありませんが、少なくとも機能します。

階層型ステートマシン

ヒロインの行動をさらに活性化すると、おそらく同じような状態が大量に発生するでしょう。 たとえば、立ったり、歩いたり、走ったり、坂道を滑り落ちたりすることはできません。 これらの状態のいずれでも、B を押すとジャンプし、B を押すとしゃがみます。

ステート マシンの最も単純な実装では、すべての状態に対してこのコードを複製しました。 しかし、もちろん、コードを一度書くだけで済み、その後はすべての状態で再利用できた方がずっと良いでしょう。

これがステート マシンではなく単なるオブジェクト指向のコードである場合は、継承と呼ばれる状態間でコードを分離する手法を使用できます。 ジャンプやしゃがみを処理する基底状態のクラスを定義できます。 立つ、歩く、走る、転がるなどの動作が継承され、独自の動作が追加されます。

この決定は良い結果と悪い結果の両方をもたらします。 継承はコードを再利用するための強力なツールですが、同時に 2 つのコード間に非常に強力な結合を提供します。 ハンマーは軽率に叩くには重すぎます。

この形式では、結果として得られる構造は次のように呼ばれます。 階層型ステートマシン(または 階層型オートマトン)。 そして、各条件には独自のものが存在する可能性があります スーパーステート(状態自体は サブステート)。 イベントが発生し、サブステートがそれを処理しない場合、イベントはスーパーステートのチェーンに渡されます。 つまり、継承されたメソッドのオーバーライドのように見えます。

実際、元の State パターンを使用して FSM を実装すると、すでにクラス継承を使用して階層を実装できます。 スーパークラスの基本クラスを定義しましょう。

class OnGroundState: public HeroineState ( public : 仮想ボイドハンドル入力 (ヒロイン&ヒロイン、入力入力)( if (input == PRESS_B) ( // Jump... ) else if (input == PRESS_DOWN) ( // Squat... ) ) );

そして各サブクラスはそれを継承します。

class DuckingState: public OnGroundState ( public : 仮想ボイドハンドル入力 (ヒロイン&ヒロイン、入力入力)( if (input == RELEASE_DOWN) ( // 起きてください... ) else ( // 入力は処理されません。 したがって、それを階層の上位に渡します。 OnGroundState::handleInput(ヒロイン, 入力); ) ) );

もちろん、これが階層を実装する唯一の方法ではありません。 ただし、Gang of Four State テンプレートを使用しない場合は機能しません。 代わりに、次のようにして、現在の状態とスーパーステートの明確な階層をモデル化できます。 スタックメインクラスの単一の状態ではなく、複数の状態。

現在の状態がスタックの最上位にあり、その下にそのスーパーステートがあり、その下にそのスーパーステートが配置されます。 これスーパーステートなど また、状態固有の動作を実装する必要がある場合は、スタックの最上位から開始して、状態が処理するまで下に向かって作業します。 (処理されない場合は、単純に無視します)。

マガジンメモリ付き自動機

ステート マシンには、ステート スタックを使用する別の一般的な拡張機能もあります。 ここでのみ、スタックはまったく異なる概念を表し、さまざまな問題を解決するために使用されます。

問題は、ステートマシンには概念がないことです 物語。 自分がどの状態にいるか知っていますか? あなたは, しかし、自分がどのような状態にあるのかについての情報はありません。 だった。 したがって、以前の状態に戻す簡単な方法はありません。

簡単な例を次に示します。以前は、恐れを知らぬヒロインが全力で武装することを許可していました。 彼女が武器を発砲するとき、発砲アニメーションを再生し、弾丸を生成し、付随する視覚効果を行うための新しいステートが必要です。 これを行うには、新しい FiringState を作成し、ヒロインが発射ボタンを押すことで発砲できるすべての状態からそれに遷移します。

この動作は複数の状態間で複製されるため、ここで階層ステート マシンを使用してコードを再利用できます。

ここで難しいのは、どの状態に移行する必要があるのか​​を何らかの方法で理解する必要があることです。 撮影。 ヒロインは静止しているとき、走っているとき、ジャンプしているとき、またはしゃがんでいるときにクリップ全体を発射できます。 撮影シーケンスが終了したら、撮影前の状態に戻る必要があります。

純粋な FSM に執着すると、自分がどのような状態にあったかをすぐに忘れてしまいます。 これを追跡するには、立っている射撃、走っている射撃、ジャンプしている射撃など、ほぼ同じ状態を多数定義する必要があります。 したがって、完了時に正しい状態に移行する遷移がハードコーディングされています。

本当に必要なのは、撮影前の状態を保存し、撮影後に再度思い出す能力です。 ここでもオートマトンの理論が役に立ちます。 対応するデータ構造はプッシュダウン オートマトンと呼ばれます。

有限オートマトンでは状態へのポインタが 1 つだけですが、ストア メモリを持つオートマトンでは状態へのポインタが存在します。 スタック。 FSM では、新しい状態への遷移により前の状態が置き換えられます。 マガジン メモリを備えたマシンでもこれを行うことができますが、さらに 2 つの操作が追加されます。

    あなたはできる 場所 (押す) 新しい状態をスタックに追加します。 現在の状態は常にスタックの最上位にあるため、これは新しい状態に移行する操作です。 しかし同時に、古い状態はスタック上の現在の状態の直下に残り、跡形もなく消えることはありません。

    あなたはできる 抽出する (ポップ) スタックの最上位状態。 状態は消え、その下にあったものが現在のものになります。

撮影に必要なのはこれだけです。 私たちが作成します 唯一のもの撮影状況。 別の状態で発射ボタンを押すと、 場所 (押す)スタック射撃状態。 撮影アニメーションが終了すると、 抽出する (ポップ) 状態に戻り、マガジン メモリを備えたマシンは自動的に前の状態に戻ります。

それらは実際にどれほど役立つのでしょうか?

このようにステート マシンが拡張されても、その機能は依然としてかなり制限されています。 今日の AI では、次のようなものを使用するのが一般的な傾向です。 ビヘイビアツリー(ビヘイビアツリー) と 計画システム(計画システム)。 AI の分野に特に興味がある場合は、この章全体が食欲をそそるはずです。 彼を満足させるには、他の本に目を向ける必要があります。

これは、有限状態マシン、マガジン メモリを備えたマシン、およびその他の同様のシステムがまったく役に立たないことを意味するものではありません。 いくつかの点では、これらは優れたモデリング ツールです。 ステート マシンは次の場合に役立ちます。

  • 内部状態に応じて動作が変化するエンティティがあります。
  • この条件は、比較的少数の特定のオプションに厳密に分割されます。
  • エンティティは、一連の入力コマンドまたはイベントに継続的に応答します。

ゲームでは、ステート マシンは通常 AI のモデル化に使用されますが、ユーザー入力、メニュー ナビゲーション、テキスト解析、ネットワーク プロトコル、その他の非同期動作の実装にも使用できます。

"パターン州"ソース.ru

状態は、オブジェクトの内部状態に応じて異なる機能を指定するオブジェクトの動作のパターンです。 ウェブサイト ウェブサイトのソース オリジナル

条件、タスク、目的

オブジェクトの内部状態に応じて動作を変えることができます。 挙動は何の制限もなく完全に任意に変化するため、外から見るとオブジェクトのクラスが変化したように見えます。

モチベーション

クラスを考慮してください TCP接続、ネットワーク接続を表します。 このクラスのオブジェクトは、次のいずれかの状態になります。 設立(インストール済み)、 聞いている(聞いている)、 閉まっている(閉まっている)。 オブジェクトのとき TCP接続他のオブジェクトからリクエストを受信すると、現在の状態に応じて異なる応答を返します。 たとえば、リクエストに対するレスポンス 開ける(オープン) は接続が状態かどうかによって決まります 閉まっているまたは 設立。 状態パターンは、オブジェクトがどのように動作するかを記述します。 TCP接続異なる状態では異なる動作をする可能性があります。 サイトソース 元のサイト

このパターンの主なアイデアは、抽象クラスを導入することです。 TCP状態さまざまな接続状態を表します。 このクラスは、さまざまなワーカーを記述するすべてのクラスに共通のインターフェイスを宣言します。 オリジナルソース.ru

状態。 これらのサブクラスでは TCP状態状態固有の動作が実装されます。 たとえば、授業では TCP確立済みそして TCP閉まっている状態固有の動作が実装されました 設立そして 閉まっているそれぞれ。 ウェブサイト ウェブサイトのオリジナルソース

オリジナル.ru

クラス TCP接続状態オブジェクト (サブクラスのインスタンス) を格納します。 TCP状態) 接続の現在の状態を表し、状態に依存するすべてのリクエストをこのオブジェクトに委任します。 TCP接続サブクラスの独自のインスタンスを使用します TCP状態非常に単純です: 単一のインターフェースのメソッドを呼び出す TCP状態、現在格納されている特定のサブクラスにのみ依存します TCP状態-a - 結果は異なります。つまり、 実際には、この接続状態にのみ固有の操作が実行されます。 オリジナル.ruソース

そして接続状態が変化するたびにTCP接続状態オブジェクトを変更します。 たとえば、確立された接続が閉じられると、 TCP接続クラスのインスタンスを置き換えます TCP確立済みコピー TCP閉まっている. サイトのオリジナルソースサイト

適用の兆候、State パターンの使用

次の場合に状態パターンを使用します:source.ru
  1. オブジェクトの動作がその状態に依存し、実行時に変更する必要がある場合。 ソースoriginal.ru
  2. オペレーションコードに多数の分岐からなる条件文が含まれており、分岐の選択が状態に依存する場合。 通常、この場合、状態は列挙された定数によって表されます。 多くの場合、同じ条件ステートメント構造が複数の操作で繰り返され、状態パターンでは各ブランチを別個のクラスに配置することが推奨されます。 これにより、オブジェクトの状態を、他のオブジェクトとは独立して変更できる独立したオブジェクトとして扱うことができます。 サイトソース 元のサイト

解決

ウェブサイト ウェブサイトのオリジナルソース

ソース.ru

州パターンの参加者

ソース.ru
  1. コンテクスト(TCPConnection) - コンテキスト。
    クライアント用の単一インターフェイスを定義します。
    サブクラスのインスタンスを格納します コンクリートの状態、現在の状態を決定します。 コードラボ。
  2. (TCPState) - 状態。
    特定のコンテキスト状態に関連付けられた動作をカプセル化するためのインターフェイスを定義します。 サイトソース サイトオリジナル
  3. サブクラス コンクリートの状態(TCP確立、TCPListen、TCPClosed) - 特定の状態。
    各サブクラスは、何らかのコンテキスト状態に関連付けられた動作を実装します。 コンテクスト. サイトのオリジナルソースサイト

Stateパターンを使用するためのスキーム

クラス コンテクストリクエストを現在のオブジェクトに委任します コンクリートの状態. サイトのオリジナルソースサイト

コンテキストはそれ自体を引数としてオブジェクトに渡すことができます 、リクエストが処理されます。 これにより、状態オブジェクト ( コンクリートの状態) 必要に応じてコンテキストにアクセスします。 コードラボ。

コンテクスト- これはクライアントのメイン インターフェイスです。 クライアントは状態オブジェクトを使用してコンテキストを構成できます (より正確に コンクリートの状態)。 コンテキストが設定されると、クライアントは状態オブジェクトと直接通信する必要がなくなります (共通インターフェイスを介してのみ通信する必要があります)。 )。 ソース.ru

この場合、どちらか コンテクスト、またはサブクラス自体 コンクリートの状態どのような条件下で、どのような順序で状態の変化が起こるかを決定できます。 ウェブサイト ウェブサイトのソース オリジナル

State パターンの実装に関する質問

State パターンの実装に関する質問: ソースoriginal.ru
  1. 状態間の遷移を決定するもの。
    状態パターンは、どの参加者が状態間の遷移の条件 (基準) を決定するかについては何も述べません。 基準が固定されている場合は、クラスに直接実装できます。 コンテクスト。 ただし、一般に、より柔軟で正しいアプローチは、クラス自体のサブクラスを許可することです。 次の状態と移行の瞬間を決定します。 これを授業で行うには コンテクストオブジェクトから許可するインターフェースを追加する必要があります その状態を設定します。
    この分散型移行ロジックは変更と拡張が簡単です。新しいサブクラスを定義するだけで済みます。 。 分散化の欠点は、各サブクラスが 別の状態の少なくとも 1 つのサブクラス (実際に現在の状態を切り替えることができる) について「知っている」必要があり、これによりサブクラス間に実装の依存関係が生じます。 サイトソース サイトオリジナル

    ソースoriginal.ru
  2. 表形式の代替案。
    ステート駆動型コードを構造化する別の方法もあります。 これが有限状態マシンの原理です。 テーブルを使用して入力を状態遷移にマッピングします。 これを利用すると、特定の入力データが到着したときにどの状態に移行する必要があるかを決定できます。 基本的に、条件付きコードをテーブル検索に置き換えています。
    このマシンの主な利点はその規則性です。移行基準を変更するには、コードではなくデータのみを変更するだけで十分です。 しかし、デメリットもあります。
    - テーブルの検索は、関数を呼び出すよりも効率が悪いことがよくあります。
    - 移行ロジックを統一した表形式で提示すると、基準が明確でなくなるため、理解が難しくなります。
    - 通常、状態間の遷移を伴うアクションを追加するのは困難です。 表形式の方法では状態と状態間の遷移が考慮されますが、状態の変化ごとに任意の計算を実行できるように補足する必要があります。
    テーブルベースのステート マシンとパターン ステートの主な違いは、次のように定式化できます。パターン ステートは状態依存の動作をモデル化し、テーブル メソッドはステート間の遷移の定義に焦点を当てます。 オリジナル.ru

    サイトのオリジナルソースサイト
  3. 状態オブジェクトの作成と破棄。
    開発プロセスでは通常、次のいずれかを選択する必要があります。
    - 必要なときに状態オブジェクトを作成し、使用後すぐに破棄します。
    - 事前に永久に作成します。

    システムがどのような状態に陥るかが事前に不明であり、コンテキストによって状態が変化することは比較的まれである場合には、最初のオプションが推奨されます。 同時に、決して使用されないオブジェクトは作成しません。これは、多くの情報が状態オブジェクトに保存されている場合に重要です。 状態の変更が頻繁に発生し、それを表すオブジェクトを破棄したくない場合 (すぐに再び必要になる可能性があるため)、2 番目の方法を使用する必要があります。 オブジェクトの作成にかかる時間は最初の 1 回だけであり、破壊にはまったく時間がかかりません。 確かに、コンテキストにはシステムが理論的に陥り得るすべての状態への参照を格納する必要があるため、このアプローチは不便であることが判明する可能性があります。 ソースoriginal.ru

    source.ruオリジナル
  4. 動的変更を使用します。
    実行時にオブジェクトのクラスを変更することで、オンデマンドの動作を変更することができますが、ほとんどのオブジェクト指向言語はこれをサポートしていません。 例外は、そのようなメカニズムを提供し、したがってパターン状態を直接サポートする Perl、JavaScript、およびその他のスクリプト エンジン ベースの言語です。 これにより、オブジェクトのクラス コードを変更することで動作を変えることができます。 ソース.ru

    .ruソースオリジナル

結果

使用結果 パターンの状態: オリジナル.ruソース
  1. 状態に依存する動作をローカライズします。
    そしてそれを状態に対応する部分に分割します。 状態パターンは、特定の状態に関連付けられたすべての動作を別のオブジェクトに入れます。 状態に依存するコードは完全にクラスのサブクラスの 1 つに含まれているため、 そうすると、新しいサブクラスを生成するだけで、新しい状態と遷移を追加できます。
    代わりに、データ メンバーを使用して内部状態を定義し、その後オブジェクトの操作を定義することもできます。 コンテクストこのデータをチェックします。 ただし、この場合、同様の条件ステートメントまたは分岐ステートメントがクラス コード全体に散在することになります。 コンテクスト。 ただし、新しい状態を追加するにはいくつかの操作を変更する必要があり、メンテナンスが困難になります。 状態パターンはこの問題を解決しますが、異なる状態の動作が最終的に複数のサブクラスに分散されるため、別の問題も発生します。 。 これによりクラス数が増加します。 もちろん、1 つのクラスの方がコンパクトですが、状態が多数ある場合は、そのような分散の方が効率的です。そうしないと、面倒な条件文を処理する必要があるからです。
    面倒な条件文があることは、長いプロシージャと同様に望ましくありません。 これらはモノリシックすぎるため、コードの変更と拡張が問題になります。 状態パターンは、状態に依存するコードを構造化するためのより良い方法を提供します。 状態遷移を記述するロジックはモノリシック ステートメントにラップされなくなりました もしまたは スイッチ、ただしサブクラス間で分散 。 各遷移とアクションをクラスにカプセル化することで、状態は本格的なオブジェクトになります。 これにより、コードの構造が改善され、コードの目的がより明確になります。 オリジナル.ru
  2. 状態間の遷移を明示的にします。
    オブジェクトが内部データのみの観点から現在の状態を定義する場合、状態間の遷移には明示的な表現がありません。 これらは、特定の変数への代入としてのみ表示されます。 異なる状態に個別のオブジェクトを導入すると、遷移がより明確になります。 さらに、オブジェクト コンテキストを保護できる コンテクストコンテキストの観点からの遷移はアトミックなアクションであるため、内部変数の不一致によるものです。 遷移するには、1 つの変数 (オブジェクト変数) の値のみを変更する必要があります。 クラスで コンテクスト)、複数ではなく。 ソース.ru
  3. 状態オブジェクトは共有できます。
    状態オブジェクト内の場合 インスタンス変数はありません。つまり、それが表す状態は型自体によってのみエンコードされ、異なるコンテキストが同じオブジェクトを共有できます。 。 このように国家が分離されると、それらの国家は本質的には内部国家を持たず、行動だけを持つ日和見主義者になります(日和見主義者のパターンを参照)。 サイトソース 元のサイト

「」セクションの例の実装を見てみましょう。 いくつかの単純な TCP 接続アーキテクチャを構築します。 これは TCP プロトコルの簡略化されたバージョンですが、もちろん、プロトコル全体や TCP 接続のすべての状態を表すものではありません。 source.ruオリジナル

まずはクラスを定義しましょう TCP接続、データ転送用のインターフェイスを提供し、状態変更リクエストを処理します: TCPConnection。 source.ruオリジナル

メンバー変数内 クラス TCP接続クラスのインスタンスが保存される TCP状態。 このクラスは、クラスで定義された状態変更インターフェイスを複製します。 TCP接続. オリジナル.ruソース

サイトのオリジナルソースサイト

TCP接続状態に依存するすべてのリクエストを状態に格納されているインスタンスに委任します。 TCP状態。 授業中も TCP接続手術があります 状態の変更、これを使用して、別のオブジェクトへのポインタをこの変数に書き込むことができます TCP状態。 クラスコンストラクター TCP接続初期化します 閉じた状態へのポインタ TCP閉まっている(以下で定義します)。 ソースoriginal.ru

source.ruオリジナル

あらゆる操作 TCP状態インスタンスを受け入れます TCP接続パラメータとして使用することで、オブジェクトを許可します TCP状態オブジェクトデータにアクセスする TCP接続接続状態を変更します。 .ru

クラスで TCP状態委任されたすべてのリクエストに対してデフォルトの動作を実装しました。 オブジェクトの状態を変更することもできます TCP接続手術を通して 状態の変更. TCP状態と同じパッケージ内にあります TCP接続なので、この操作 TCPState にもアクセスできます。 サイトのオリジナルソースサイト

オリジナル.ru

サブクラス内 TCP状態状態依存の動作が実装されました。 TCP 接続にはさまざまな状態が考えられます。 設立(インストール済み)、 聞いている(聞いている)、 閉まっている(closed) など、それぞれに独自のサブクラスがあります。 TCP状態。 簡単にするために、3 つのサブクラスのみを詳細に検討します。 TCP確立済み, TCPリッスンそして TCP閉まっている. ウェブサイト ウェブサイトのオリジナルソース

ルのソース

サブクラス内 TCP状態は、その状態で有効なリクエストに対して状態に依存した動作を実装します。 .ru

サイトのオリジナルソースサイト

状態固有のアクションを実行した後、これらの操作は ソースoriginal.ru

原因 状態の変更オブジェクトの状態を変更する TCP接続。 彼自身は TCP プロトコルに関する情報を持っていません。 サブクラスです TCP状態プロトコルによって指示された状態とアクションの間の遷移を定義します。 オリジナル.ru

ソースoriginal.ru

状態パターンの既知の応用例

Ralph Johnson と Jonathan Zweig は、状態パターンを特徴づけ、TCP プロトコルに関連して説明します。
最も一般的な対話型描画プログラムは、直接操作操作を実行するための「ツール」を提供します。 たとえば、線描画ツールを使用すると、ユーザーはマウスで任意の点をクリックし、マウスを移動してその点から線を引くことができます。 選択ツールを使用すると、いくつかの図形を選択できます。 通常、使用可能なツールはすべてパレットに配置されます。 ユーザーの仕事はツールを選択して適用することですが、実際には、ツールが変わるとエディターの動作も変化します。描画ツールでは形状を作成し、選択ツールでは形状を選択するなどです。 ウェブサイト ウェブサイトのオリジナルソース

エディターの動作の現在のツールへの依存を反映するには、状態パターンを使用できます。 オリジナルソース.ru

抽象クラスを定義できる 道具、そのサブクラスはツール固有の動作を実装します。 グラフィカルエディタは現在のオブジェクトへのリンクを保存します あまりにもそして受信したリクエストを彼に委任します。 ツールを選択すると、エディターは別のオブジェクトを使用するため、動作が変わります。 .ru

この技術は、HotDraw および Unidraw グラフィック エディタのフレームワークで使用されます。 これにより、顧客は新しいタイプのツールを簡単に定義できるようになります。 で ホットドロークラス 描画コントローラリクエストを現在のオブジェクトに転送します 道具。 で ユニドロー対応するクラスが呼び出されます ビューアそして 道具。 以下のクラス図は、クラス インターフェイスの概略図を示しています。 道具

オリジナル.ru

State パターンの目的

  • State パターンを使用すると、オブジェクトの内部状態に応じて動作を変更できます。 オブジェクトのクラスが変更されたようです。
  • State パターンは、ステート マシンのオブジェクト指向実装です。

解決すべき問題

オブジェクトの動作はその状態に依存し、プログラムの実行中に変更する必要があります。 このようなスキームは、多くの条件演算子を使用して実装できます。オブジェクトの現在の状態の分析に基づいて、特定のアクションが実行されます。 ただし、状態の数が多いと、条件ステートメントがコード全体に散在し、そのようなプログラムの保守が困難になります。

状態パターンの議論

State パターンは、この問題を次のように解決します。

  • 外部へのインターフェースを定義する Context クラスを導入します。
  • 抽象 State クラスを導入します。
  • ステート マシンのさまざまな「状態」を State サブクラスとして表します。
  • Context クラスには、ステート マシンの状態が変化すると変化する現在の状態へのポインターがあります。

状態パターンは、新しい状態への遷移条件が正確にどこで決定されるかを定義しません。 Context クラスまたは State サブクラスの 2 つのオプションがあります。 後者のオプションの利点は、新しい派生クラスを簡単に追加できることです。 欠点は、各 State サブクラスが新しい状態に移行するために隣接するサブクラスについて認識する必要があり、サブクラス間に依存関係が生じることです。

入力データを状態間の遷移に一意にマッピングするテーブルの使用に基づいて、有限状態マシンを設計する別のテーブルベースのアプローチもあります。 ただし、このアプローチには欠点があります。トランジションの実行時にアクションの実行を追加するのが難しいということです。 状態パターンのアプローチでは、(データ構造の代わりに) コードを使用して状態間の遷移を行うため、これらのアクションを簡単に追加できます。

状態パターンの構造

Context クラスは、クライアントの外部インターフェイスを定義し、State オブジェクトの現在の状態への参照を保存します。 抽象基本クラス State のインターフェイスは、1 つの追加パラメーター (Context インスタンスへのポインター) を除いて Context インターフェイスと同じです。 状態派生クラスは、状態固有の動作を定義します。 Context ラッパー クラスは、受信したすべてのリクエストを「現在の状態」オブジェクトに委任します。このオブジェクトは、受信した追加パラメーターを使用して Context インスタンスにアクセスできます。

State パターンを使用すると、オブジェクトの内部状態に応じて動作を変更できます。 自動販売機の動作でも同様の状況が観察されます。 マシンは、商品の入手可能性、受け取ったコインの量、両替機能などに応じてさまざまな状態になることがあります。 購入者が製品を選択して支払いを行った後、次のような状況 (状態) が考えられます。

  • 商品を購入者に渡します。小銭は必要ありません。
  • 購入者に商品と小銭を渡します。
  • 十分なお金がないため、購入者は商品を受け取ることができません。
  • 購入者は不在のため商品を受け取ることができません。

状態パターンの使用

  • クライアントが「ステート マシン」として使用する既存の Context ラッパー クラスを定義するか、新しい Context ラッパー クラスを作成します。
  • Context クラスのインターフェイスを複製する基本 State クラスを作成します。 各メソッドは追加パラメータを 1 つ取ります。それは Context クラスのインスタンスです。 State クラスは、便利な「デフォルト」動作を定義できます。
  • 考えられるすべての状態に対して状態派生クラスを作成します。
  • Context ラッパー クラスには、現在の状態オブジェクトへの参照があります。
  • Context クラスは、クライアントから受信したすべてのリクエストを「現在の状態」オブジェクトに単純に委任し、Context オブジェクトのアドレスが追加パラメータとして渡されます。
  • このアドレスを使用して、State クラスのメソッドは、必要に応じて Context クラスの「現在の状態」を変更できます。

Stateパターンの特徴

  • 状態オブジェクトは多くの場合シングルトンです。
  • Flyweight は、State オブジェクトをいつどのように分割できるかを示します。
  • Interpreter パターンは、State を使用して解析コンテキストを定義できます。
  • State パターンと Bridge パターンは同様の構造を持っていますが、Bridge ではエンベロープ クラス (「ラッパー」クラスの類似物) の階層が許可されるのに対し、State では許可されない点が異なります。 これらのパターンは類似した構造を持っていますが、解決する問題は異なります。状態ではオブジェクトがその内部状態に応じて動作を変更できるのに対し、ブリッジでは抽象化がその実装から分離されているため、相互に独立して変更できます。
  • State パターンの実装は、Strategy パターンに基づいています。 違いはその目的にあります。

Stateパターンの実装

2 つの可能な状態と 2 つのイベントを持つ有限状態マシンの例を考えてみましょう。

#含む 名前空間 std を使用します。 class Machine ( class State *current; public: Machine(); void setCurrent(State *s) ( current = s; ) void on(); void off(); ); class State ( public: virtual void on(Machine *m) ( cout<< " already ON\n"; } virtual void off(Machine *m) { cout << " already OFF\n"; } }; void Machine::on() { current->(これ); ) void Machine::off() ( current->off(this); ) class ON: public State ( public: ON() ( cout<< " ON-ctor "; }; ~ON() { cout << " dtor-ON\n"; }; void off(Machine *m); }; class OFF: public State { public: OFF() { cout << " OFF-ctor "; }; ~OFF() { cout << " dtor-OFF\n"; }; void on(Machine *m) { cout << " going from OFF to ON"; m->setCurrent(new ON()); これを削除します。 ) ); void ON::off(Machine *m) ( cout<< " going from ON to OFF"; m->setCurrent(新しいOFF()); これを削除します。 ) Machine::Machine() ( current = new OFF(); cout<< "\n"; } int main() { void(Machine:: *ptrs)() = { Machine::off, Machine::on }; Machine fsm; int num; while (1) { cout << "Enter 0/1: "; cin >>番号; (fsm.*ptrs)(); ))

15.02.2016
21:30

State パターンは、複数の独立した論理状態を持つクラスを設計することを目的としています。 早速例に入ってみましょう。

Web カメラ コントロール クラスを開発しているとします。 カメラは次の 3 つの状態になります。

  1. 初期化されていません。 これを NotConnectedState と呼びましょう。
  2. 初期化され準備完了ですが、フレームはまだキャプチャされていません。 ReadyState にしておきます。
  3. アクティブなフレーム キャプチャ モード。 ActiveState と表します。

ここでは状態パターンを使用しているため、状態図のイメージから始めるのが最善です。

では、この図をコードに変換してみましょう。 実装が複雑にならないように、Web カメラを操作するコードは省略しています。 必要に応じて、適切なライブラリ関数呼び出しを自分で追加できます。

最小限のコメントを付けて完全なリストをすぐに提供します。 次に、この実装の重要な詳細について詳しく説明します。

#含む #define DECLARE_GET_INSTANCE(ClassName) \ static ClassName* getInstance() (\ static ClassName インスタンス;\ return \ ) class WebCamera ( public: typedef std::string Frame; public: // *********** *************************************** // 例外 // ****** ********************************************* クラス NotSupported: パブリック標準: :Exception ( ); public: // ***************************************** ******* ********* // 州 // ***************************** ******* ************** class NotConnectedState; class ReadyState; class ActiveState; class State ( public: virtual ~State() ( ) virtual void connect(WebCamera*) ( throw NotSupported(); ) virtual voiddetach(WebCamera* cam) (std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->ChangeState(NotConnectedState::getInstance()); ) virtual void start(WebCamera*) ( throw NotSupported(); ) virtual void stop(WebCamera*) ( throw NotSupported(); ) virtual Frame getFrame(WebCamera*) ( throw NotSupported(); ) protected: State() ( ) ); // *********************************************** ** class NotConnectedState: public State ( public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(WebCamera* cam) ( std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->ChangeState(ReadyState::getInstance()); ) void connect(WebCamera*) ( throw NotSupported(); ) private: NotConnectedState() ( ) ); // *********************************************** ** class ReadyState: public State ( public: DECLARE_GET_INSTANCE(ReadyState) void start(WebCamera* cam) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->ChangeState(ActiveState::getInstance()); ) プライベート: ReadyState() ( ) ); // *********************************************** ** class ActiveState: public State ( public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam-> << "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } }; public: explicit WebCamera(int camID) : m_camID(camID), m_state(NotConnectedState::getInstance()) { } ~WebCamera() { try { disconnect(); } catch(const NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } } void connect() { m_state->接続(これ); ) void cancel() ( m_state->disconnect(this); ) void start() ( m_state->start(this); ) void stop() ( m_state->stop(this); ) Frame getFrame() ( return m_state ->getFrame(this); ) private: void changeState(State* newState) ( m_state = newState; ) private: int m_camID; 状態* m_state; );

DECLARE_GET_INSTANCE マクロに注目してください。 もちろん、C++ でマクロを使用することはお勧めできません。 ただし、これはマクロがテンプレート関数の類似物として機能する場合に当てはまります。 この場合、常に後者を優先してください。

私たちの場合、マクロは の実装に必要な静的関数を定義することを目的としています。 したがって、その使用は正当であると考えられます。 結局のところ、コードの重複を減らすことができ、深刻な脅威を引き起こすことはありません。

State クラスをメイン クラス WebCamera で宣言します。 簡潔にするために、すべてのクラスのメンバー関数のインライン定義を使用しました。 ただし、実際のアプリケーションでは、宣言と実装を h および cpp ファイルに分離することに関する推奨事項に従うことをお勧めします。

状態クラスは WebCamera 内で宣言されるため、そのクラスのプライベート フィールドにアクセスできます。 もちろん、これにより、これらすべてのクラス間に非常に緊密な接続が作成されます。 しかし、州は非常に特殊であることが判明したため、他の状況で再利用することは問題外です。

状態クラスの階層の基礎は、抽象クラス WebCamera::State: です。

クラス状態 ( public: virtual ~State() ( ) virtual void connect(WebCamera*) ( throw NotSupported(); ) virtual voiddetach(WebCamera* cam) ( std::cout<< "Деинициализируем камеру..." << std::endl; // ... cam->ChangeState(NotConnectedState::getInstance()); ) virtual void start(WebCamera*) ( throw NotSupported(); ) virtual void stop(WebCamera*) ( throw NotSupported(); ) virtual Frame getFrame(WebCamera*) ( throw NotSupported(); ) protected: State() ( ) );

そのメンバー関数はすべて、WebCamera クラス自体の関数に対応します。 直接委任が発生します。

Class WebCamera ( // ... void connect() ( m_state->connect(this); ) void Connect() ( m_state->disconnect(this); ) void start() ( m_state->start(this); ) void stop() ( m_state->stop(this); ) Frame getFrame() ( return m_state->getFrame(this); ) // ... State* m_state; )

重要な機能は、State オブジェクトが、それを呼び出す WebCamera インスタンスへのポインターを受け入れることです。 これにより、任意の数のカメラに対して、State オブジェクトを 3 つだけ持つことができます。 この可能性は、Singleton パターンを使用することで実現されます。 もちろん、この例の文脈では、これによって大きな利益が得られるわけではありません。 しかし、このテクニックを知っていることは依然として役に立ちます。

WebCamera クラス自体は、事実上何も行いません。 彼は完全に州に依存しています。 そして、これらの国家は、作戦を実行するための条件を決定し、必要なコンテキストを提供します。

ほとんどの WebCamera::State メンバー関数は独自の WebCamera::NotSupported をスローします。 これは完全に適切なデフォルトの動作です。 たとえば、すでに初期化されているカメラを初期化しようとすると、ごく自然に例外が発生します。

同時に、WebCamera::State::disconnect() のデフォルト実装を提供します。 この動作は、3 つの状態のうち 2 つに適しています。 その結果、コードの重複が防止されます。

状態を変更するには、プライベート メンバー関数 WebCamera::changeState() を使用します。

Void changeState(State* newState) ( m_state = newState; )

次に、特定の状態の実装に移ります。 WebCamera::NotConnectedState の場合は、connect() 操作とdisconnect() 操作をオーバーライドするだけで十分です。

クラス NotConnectedState: public State ( public: DECLARE_GET_INSTANCE(NotConnectedState) void connect(WebCamera* cam) ( std::cout<< "Инициализируем камеру..." << std::endl; // ... cam->ChangeState(ReadyState::getInstance()); ) void connect(WebCamera*) ( throw NotSupported(); ) private: NotConnectedState() ( ) );

州ごとに 1 つのインスタンスを作成できます。 これは、プライベート コンストラクターを宣言することで保証されます。

提示された実装のもう 1 つの重要な要素は、成功した場合にのみ新しい状態に移行することです。 たとえば、カメラの初期化中に障害が発生した場合、ReadyState に入るには早すぎます。 主なアイデアは、カメラの実際の状態 (この場合) と State オブジェクトの間の完全な対応です。

これでカメラの準備は完了です。 対応する WebCamera::ReadyState State クラスを作成しましょう。

クラス ReadyState: public State ( public: DECLARE_GET_INSTANCE(ReadyState) void start(WebCamera* cam) ( std::cout<< "Запускаем видео-поток..." << std::endl; // ... cam->ChangeState(ActiveState::getInstance()); ) プライベート: ReadyState() ( ) );

準備完了状態から、アクティブなフレーム キャプチャ状態に入ることができます。 この目的のために、start() オペレーションが提供されており、これを実装しました。

ついに、カメラの最後の論理状態、WebCamera::ActiveState に到達しました。

クラス ActiveState: public State ( public: DECLARE_GET_INSTANCE(ActiveState) void stop(WebCamera* cam) ( std::cout<< "Останавливаем видео-поток..." << std::endl; // ... cam->ChangeState(ReadyState::getInstance()); ) フレーム getFrame(WebCamera*) ( std::cout<< "Получаем текущий кадр..." << std::endl; // ... return "Current frame"; } private: ActiveState() { } };

この状態では、 stop() を使用してフレームのキャプチャを停止できます。 その結果、WebCamera::ReadyState に戻ります。 さらに、カメラのバッファーに蓄積されたフレームを受信できます。 簡単にするために、「フレーム」とは通常の文字列を意味します。 実際には、これはある種のバイト配列になります。

これで、WebCamera クラスを操作する典型的な例を書き留めることができます。

Int main() ( WebCamera cam(0); try ( // カメラは NotConnectedState cam.connect(); // カメラは ReadyState cam.start(); // カメラは ActiveState std::cout<< cam.getFrame() << std::endl; cam.stop(); // Можно было сразу вызвать disconnect() // cam в Состоянии ReadyState cam.disconnect(); // cam в Состоянии NotConnectedState } catch(const WebCamera::NotSupported& e) { // Обрабатываем исключение } catch(...) { // Обрабатываем исключение } return 0; }

結果として、コンソールに次のように出力されます。

カメラを初期化します... ビデオ ストリームを開始します... 現在のフレームを取得します... 現在のフレーム ビデオ ストリームを停止します... カメラの初期化を解除します...

次に、エラーを引き起こしてみましょう。 connect() を 2 回続けて呼び出してみましょう。

Int main() ( WebCamera cam(0); try ( // カメラは NotConnectedState にあります cam.connect(); // カメラは ReadyState にあります // ただし、この状態では connect() 操作は提供されません! cam.connect( ); // 例外をスローします NotSupported ) catch(const WebCamera::NotSupported& e) ( std::cout<< "Произошло исключение!!!" << std::endl; // ... } catch(...) { // Обрабатываем исключение } return 0; }

そこから出てくるものは次のとおりです。

カメラを初期化しています...例外が発生しました!!! カメラの初期化を解除しましょう...

カメラはまだ初期化されていないことに注意してください。 WebCamera デストラクターで切断() 呼び出しが発生しました。 それらの。 オブジェクトの内部状態は完全に正しいままです。

結論

状態パターンを使用すると、状態図をコードに一意に変換できます。 一見すると、実装は冗長であることがわかりました。 ただし、メイン クラス WebCamera を操作する場合に考えられるコンテキストを明確に分けることができました。 その結果、個々の州を記述する際に、狭いタスクに集中することができました。 これは、明確で理解しやすく信頼性の高いコードを記述するための最良の方法です。