静的状態管理 〜第3回〜

課題の解答例

以下の3つのJSファイルを作成し、HTMLで読み込みます。
  1. クラスファイル
    前述のものなのでソースは省略
  2. 状態定義・判定


    (function() {



    //--------------------------------------
    // ラジオ - Guest
    var radioGuest = ElementManager.createInstance("RadioGuest", "UserType_Guest");



    //--------------------------------------
    // ラジオ - Login
    var radioLogin = ElementManager.createInstance("RadioLogin", "UserType_Login");



    //--------------------------------------
    // テキスト - UserName
    var textUserName = ElementManager.createInstance("UserName", "UserName");



    // 状態 "Disabled" は非活性状態
    textUserName.defineState("Disabled", function() {
      var obj = this.getElement();
      if(obj) {
        obj.disabled = true;
      }
    });



    // 状態 "Usable" は活性状態
    textUserName.defineState("Usable", function() {
      var obj = this.getElement();
      if(obj) {
        obj.disabled = false;
      }
    });



    // 状態判定
    textUserName.judgeState = function() {
      var rle = ElementManager.getInstance("RadioLogin");
      var rlo = rle ? rle.getElement() : null;
      var state = null;
      if(rlo) {
        if(rlo.checked) {
          state = "Usable";
        } else {
          state = "Disabled";
        }
      } else {
        state = "Disabled";
      }
      return state;
    };



    //--------------------------------------
    // テキスト - パスワード
    var textPassword = ElementManager.createInstance("Password", "Password");



    // 状態 "Disabled" は非活性状態
    textPassword.defineState("Disabled", function() {
      var obj = this.getElement();
      if(obj) {
        obj.disabled = true;
      }
    });



    // 状態 "Usable" は活性状態
    textPassword.defineState("Usable", function() {
      var obj = this.getElement();
      if(obj) {
        obj.disabled = false;
      }
    });



    // 状態判定
    textPassword.judgeState = function() {
      var rle = ElementManager.getInstance("RadioLogin");
      var rlo = rle ? rle.getElement() : null;
      var une = ElementManager.getInstance("UserName");
      var uno = une ? une.getElement() : null;
      var state = null;
      if(rlo && uno) {
        if(rlo.checked && (uno.value.length > 0)) {
          state = "Usable";
        } else {
          state = "Disabled";
        }
      } else {
        state = "Disabled";
      }
      return state;
    };



    //--------------------------------------
    // ボタン - OK
    var buttonOk = ElementManager.createInstance("OkButton", "Submit");



    // 状態 "Disabled" は非活性状態
    buttonOk.defineState("Disabled", function() {
      var obj = this.getElement();
      if(obj) {
        obj.disabled = true;
      }
    });



    // 状態 "Usable" は活性状態
    buttonOk.defineState("Usable", function() {
      var obj = this.getElement();
      if(obj) {
        obj.disabled = false;
      }
    });



    // 状態判定
    buttonOk.judgeState = function() {
      var rge = ElementManager.getInstance("RadioGuest");
      var rgo = rge ? rge.getElement() : null;
      var rle = ElementManager.getInstance("RadioLogin");
      var rlo = rle ? rle.getElement() : null;
      var une = ElementManager.getInstance("UserName");
      var uno = une ? une.getElement() : null;
      var pse = ElementManager.getInstance("Password");
      var pso = pse ? pse.getElement() : null;
      var state = null;
      if(rgo && rlo && uno && pso) {
        if(rgo.checked) {
          state = "Usable";
        } else if(rlo.checked && (uno.value.length > 0) && (pso.value.length > 0)) {
          state = "Usable";
        } else {
          state = "Disabled";
        }
      } else {
        state = "Disabled";
      }
      return state;
    };



    //--------------------------------------
    // ボタン - Cancel
    var buttonCancel = ElementManager.createInstance("CancelButton", "Cancel");



    })();
  3. イベント実装


    (function() {
    //--------------------------------------
    // イベント実装
    function attachEvent() {
      // オブジェクトの用意
      var rge = ElementManager.getInstance("RadioGuest");
      var rgo = rge ? rge.getElement() : null;
      var rle = ElementManager.getInstance("RadioLogin");
      var rlo = rle ? rle.getElement() : null;
      var une = ElementManager.getInstance("UserName");
      var uno = une ? une.getElement() : null;
      var pse = ElementManager.getInstance("Password");
      var pso = pse ? pse.getElement() : null;
      var boe = ElementManager.getInstance("OkButton");
      var boo = boe ? boe.getElement() : null;
      var bce = ElementManager.getInstance("CancelButton");
      var bce = bce ? bce.getElement() : null;
      // ラジオクリック
      rgo.onclick = function() {
        var une = ElementManager.getInstance("UserName");
        une.applyState(une.judgeState());
        var pse = ElementManager.getInstance("Password");
        pse.applyState(pse.judgeState());
        var boe = ElementManager.getInstance("OkButton");
        boe.applyState(boe.judgeState());
      };
      rlo.onclick = rgo.onclick;
      // UserName変更
      uno.onchange = function() {
        var pse = ElementManager.getInstance("Password");
        pse.applyState(pse.judgeState());
        var boe = ElementManager.getInstance("OkButton");
        boe.applyState(boe.judgeState());
      };
      if(document.all) {
        uno.onpropertychange = uno.onchange;
        uno.onchange = null;
      } else {
        uno.onkeydown = uno.onchange;
      }
      // Password変更
      pso.onchange = function() {
        var boe = ElementManager.getInstance("OkButton");
        boe.applyState(boe.judgeState());
      };
      if(document.all) {
        pso.onpropertychange = pso.onchange;
        pso.onchange = null;
      } else {
        pso.onkeydown = pso.onchange;
      }
    }



    //--------------------------------------
    // オンロード
    this.onload = function() {
      // オンロード実行
      var une = ElementManager.getInstance("UserName");
      une.applyState(une.judgeState());
      var pse = ElementManager.getInstance("Password");
      pse.applyState(pse.judgeState());
      var boe = ElementManager.getInstance("OkButton");
      boe.applyState(boe.judgeState());
      // イベント実装
      attachEvent();
    };



    })();

解答例の解説

まずは「状態定義・判定」ですが、
それぞれの画面要素ごとに

  1. インスタンスを作成し、
  2. 状態を定義し、
  3. 状態判定メソッドを実装

しています。
また、非活性(状態名「Disabled」)にするメソッドと活性(状態名「Usable」)にするメソッドが、どのインスタンスでも同じですから1つにまとめて共有できます。


//--------------------------------------
// 汎用状態変化メソッド



// 非活性にする
function setDisabled_true() {
  var obj = this.getElement();
  if(obj) {
    obj.disabled = true;
  }
}



// 活性にする
function setDisabled_false() {
  var obj = this.getElement();
  if(obj) {
    obj.disabled = false;
  }
}
を作成して使うことができます。

例えば、textUserName の状態定義は



// 状態 "Disabled" は非活性状態
textUserName.defineState("Disabled", setDisabled_true);



// 状態 "Usable" は活性状態
textUserName.defineState("Usable", setDisabled_false);



のように簡略化し、さらに setDisabled_true と setDisabled_false メソッドは他でも同じように使い回しができます。
ただし、今回はサンプルですから共有できるのですが、状態遷移メソッドは一般的には共有できない場合が多いと考えておいたほうがよいと思います。
それゆえ、解答例では関数を共有せずに1つ1つ記述しています。

次に「イベント実装」ですが、
それぞれのイベントごとに

  1. どのインスタンス
  2. どのような順番で
  3. 命令(「正しい状態は何か?」→「ではそれにしなさい」)をするか

を考え、実装してゆきます。
このとき、インスタンスの変数(上記「状態定義・判定」で作成したときの変数)を直接参照するのではなく、クラス(ここではElementManager)から取得するようにします。
これには、

  • 変数のスコープが全く別のところでも、クラスさえ参照できればインスタンスが取得できる
  • インスタンスを取得するには、その変数名ではなく、画面部分に対応する文字列がわかればよい
    (つまり画面部分に名前をつけることができ、その名前さえわかればよい)

というメリットがあります。
(いちいちインスタンス取得が面倒,コードが増える等のデメリットもあります)

また、インスタンスに対する操作の順序は、インスタンスごとの依存関係に注目する必要があります。
大抵の場合は、画面上で上から下へと依存関係がありますから、上のインスタンスからから下のものへと処理していくことが大半です。

ちなみに「正しい状態は?」とわざわざ聞いてから「それにしなさい」としていて2つのメソッドに分割しているのは、デバッグなどで強制的に処理させることを可能にするためです。
また、「正しい状態は?」だけを他の箇所で利用することもできます。

⇒どうでしょうか?
イベントまたはそれから呼び出される関数で処理をゴリゴリ記述するよりは、考えやすい・わかりやすいコードになっていませんか?

次回予告

拡張性について述べたいと思います。
「○○を追加したい」そんな変更があったときのインパクトは・・・