型スクリプトによる状態機械



問題
誰がこれを知りませんか:あなたは、格納されたエントリーを捜すために単純な構成要素(例えば検索分野)を書きます.入力が変わるたびに、クエリはデータベースに送られるべきです.結果はドロップダウンで表示する必要があります.これまでのところ、良い.
これは、データベースへの接続がまだ確立できないことを覚えているときです.したがって、ブール変数DBConnectionを導入します.
次に、入力が2秒以内に行われない場合のみ、検索を送信します.それで、次の変数useristypingは導入されます.
その後、検索が結果を返さなかった限り、スピニングホイールを表示する必要があります.そして、それはこの方法で行くとあなたの*ステータス*変数の概要をかなり高速失う.dbconnect == falseとissearch == trueのような組み合わせも無効です.そして、ちょうどこれらの無効な州は後で何らかの頭痛を引き起こします、なぜならば、どうにか、構成要素はそれがもう出ることができないこの無効な状態に入るために管理します.

解決策
ステートマシンは特にコンポーネントの状態を監視するのに便利です.そして、基本的にあなたのコンポーネントは、ステートマシン以外の何ものでもありません.まず最初に、コンポーネントが取ることができるすべての状態をリストします.
const enum States {
  UNINITIALIZED,
  CONNECTING,
  CONNECTED,
  INITIALIZED,
  TYPING,
  SEARCHING,
  SHOW_RESULTS,
}

  • 初期化:初期状態

  • 接続:データベース
  • を接続する

  • 初期化:接続は確立されます、そして、コンポーネントは
  • を使う準備ができています

  • タイピング:編集フィールドは使用中です

  • 検索:入力は終了し、データベース
  • に送られる

  • 結果は以下の通りであり、結果は
  • あなたのコンポーネントは、常にこれらの状態の1つです.そして、ある状態から別の状態への遷移が定義される.たとえば、入力は初期化される前には決して来ません.コンポーネントが1つの状態から別の状態に変更された場合、特定のアクションが実行されます.たとえば、コンポーネントの準備ができたらすぐに入力フィールドを有効にする必要があります.したがって、コンポーネントの基本構造は次のようになります.
    class SearchFieldController {
      private _state: States = States.UNINITIALIZED;
    
      private _inputElement: HTMLInputElement | undefined;
      private _resultElement: HTMLDivElement | undefined;
      private _dbConnection: any;
    
      public register() {
        this._inputElement = document.querySelector(".search") as HTMLInputElement;
        this._resultElement = document.querySelector(".result") as HTMLDivElement;
        if (this._inputElement && this._resultElement) {
          this._state = States.CONNECTING;
          this._inputElement.addEventListener("change", () => {
            this._state = States.TYPING;
          });
        }
      }
    
      protected onEnterConnecting(prev: States) {
        // connect database
        this._dbConnection.connect((connected: boolean) => {
          if (connected) this._state = States.INITIALIZED;
          else this._state = States.UNINITIALIZED;
        });
      }
    
      protected onEnterInitialized(prev: States) {
        // enable search field
      }
    
      protected onEnterTyping(prev: States) {
        // set a timer to recognize that there is no further input
      }
    
      protected onEnterSearching(prev: States) {
        // show spinning wheel
      }
    
      protected onLeaveSearching(next: States) {
        // hide spinning wheel
      }
    
      protected onEnterShowResults(prev: States) {
        // show results
      }
    
      protected onLeaveShowResults(next: States) {
        // hide results
      }
    }
    
    それは既にステータスマシンのようですね.状態遷移メソッドが状態変数が変更されたときに正確に呼び出された場合、それは素晴らしいことではないでしょうか?そして、それは私たちが今していることです.

    州と変遷の定義
    まず、変数状態を監視対象として宣言する.宣言では、状態変数が取るすべての値を指定します.最初の6つの遷移は、シーケンスから生じる遷移です.最後の3つの追加遷移は、ユーザーが変更または検索中に入力または結果表示を削除することができます.
        @State([
            States.UNINITIALIZED,
            States.CONNECTING,
            States.INITIALIZED,
            States.TYPING,
            States.SEARCHING,
            States.SHOW_RESULTS
        ])
        @Transition(States.UNINITIALIZED, States.CONNECTING)
        @Transition(States.CONNECTING, States.CONNECTING)
        @Transition(States.UNINITIALIZED, States.INITIALIZED)
        @Transition(States.INITIALIZED, States.TYPING)
        @Transition(States.TYPING, States.SEARCHING)
        @Transition(States.SEARCHING, States.SHOW_RESULTS)
        @Transition(States.SHOW_RESULTS, States.INITIALIZED)
        @Transition(States.SHOW_RESULTS, States.TYPING)
        @Transition(States.SEARCHING, States.TYPING)
        private _state: States = States.UNINITIALIZED;
    

    トランジションのコールバック
    各遷移に対して適切なコールバックを呼び出す必要があります.
    
        @EnterState(States.SEARCHING)
        protected onEnterSearching(prev: States) {
            // show spinning wheel
        }
    
        @LeaveState(States.SEARCHING)
        protected onLeaveSearching(next: States) {
            // hide spinning wheel
        }
    
    

    実装
    今、我々は4つの機能を実装する必要があります.ロジックは、状態のコールバックと遷移を管理するために使用されるすべての他の関数の状態で行われます.
    装飾プロパティは、新しいゲッターとセッターで上書きされます.現在、プロパティの値が変更された場合、最初に、希望するステータス変更が可能かどうかをチェックします.もしそうならば、最初に、現在の状態の休暇コールバックは呼ばれます.その後、新しいステータスが設定され、最後に対応する入力コールバックが呼び出されます.
    class StateObject {
      allowedStates: number[] = [];
      allowedTransitions: { [key: number]: number[] } | undefined;
      enterFunctions: { [key: number]: (prev: number) => void } = {};
      leaveFunctions: { [key: number]: (next: number) => void } = {};
    }
    
    export interface StateMachine {
      __stateObject: StateObject | undefined;
      [key: string]: any;
    }
    
    export function State(states: number[]) {
      return (target: Object, propertyKey: string) => {
        let stateMachine = target as StateMachine;
        if (!stateMachine.__stateObject)
          stateMachine.__stateObject = new StateObject();
        stateMachine.__stateObject.allowedStates = states;
    
        Object.defineProperty(target, propertyKey, {
          get: function (this: any) {
            return this.__stateValue;
          },
          set: function (this: any, newValue: number) {
            if (!stateMachine.__stateObject) return;
            let oldValue = this.__stateValue;
    
            if (newValue == oldValue) return;
    
            if (stateMachine.__stateObject.allowedStates.indexOf(newValue) < 0) {
              console.log("unallowed value");
              return;
            }
    
            if (
              oldValue != null &&
              stateMachine.__stateObject.allowedTransitions &&
              stateMachine.__stateObject.allowedTransitions[oldValue] &&
              stateMachine.__stateObject.allowedTransitions[oldValue].indexOf(
                newValue
              ) < 0
            ) {
              console.log("unallowed transition");
              return;
            }
            if (
              oldValue != null &&
              stateMachine.__stateObject.leaveFunctions[oldValue]
            )
              stateMachine.__stateObject.leaveFunctions[oldValue](newValue);
            this.__stateValue = newValue;
            if (
              oldValue != null &&
              stateMachine.__stateObject.enterFunctions[newValue]
            )
              stateMachine.__stateObject.enterFunctions[newValue](oldValue);
          },
        });
      };
    }
    
    export function Transition(from: number, to: number) {
      return (target: Object, propertyKey: string) => {
        let stateMachine = target as StateMachine;
        if (!stateMachine.__stateObject)
          stateMachine.__stateObject = new StateObject();
        let stateObject = stateMachine.__stateObject;
        if (!stateObject.allowedTransitions) stateObject.allowedTransitions = {};
        if (stateObject.allowedTransitions[from] == null) {
          stateObject.allowedTransitions[from] = [];
        }
        stateObject.allowedTransitions[from].push(to);
      };
    }
    
    export function EnterState(state: number) {
      return (
        target: Object,
        propertyKey: string,
        descriptor: PropertyDescriptor
      ) => {
        let stateMachine = target as StateMachine;
        if (!stateMachine.__stateObject)
          stateMachine.__stateObject = new StateObject();
        let stateObject = stateMachine.__stateObject;
        stateObject.enterFunctions[state] = stateMachine[propertyKey] as (
          prev: number
        ) => void;
      };
    }
    
    export function LeaveState(state: number) {
      return (
        target: Object,
        propertyKey: string,
        descriptor: PropertyDescriptor
      ) => {
        let stateMachine = target as StateMachine;
        if (!stateMachine.__stateObject)
          stateMachine.__stateObject = new StateObject();
        let stateObject = stateMachine.__stateObject;
        stateObject.leaveFunctions[state] = stateMachine[propertyKey] as (
          next: number
        ) => void;
      };
    }
    

    概要
    我々は今、100行以下の一般的なステートマシンの実装を作成しました.既存の実装を使用しない理由は?それも問題を解決しただろうね.
    もちろん!しかし、デコレータを使用するアプローチは非常に魅力的です.デコレータは、任意のクラスで使用することができますし、実装の詳細から無料保つ.コードは、読みやすく、構造化されます.
    実際、私はすでに多くのプロジェクトでこの実装を使用しました.3 Dシーンではマウスやタッチコントロールに用いられる複雑なコントローラがあった.
    これはDEV . TOの私の非常に最初の記事でした.私は、私がこのアプローチであなたの何人かを鼓舞することができることを望みます.それは私を幸せにします.
    私はタイプスクリプトのデコレーターが好きです、そして、私はもう少し少し「小さいヘルパー」を実行しました.それで、あなたが興味があるならば、知らせてください?
    歓声