静的状態管理 〜第4回〜

機能追加の例(課題)

下記のチェックボックスを5つ追加して、
service □News □BBS □RSS □Mail □Schedule
・GuestのときはNewsかBBSのみ選択可能にし、残りはチェックを外して非活性にする。
・1つも選択されていないときはOKボタンは非活性になる。
(これらはログイン後にどのようなサービスを求めるかの選択と仮定する)
という条件を加えることとします。


機能追加の例(考え方)

大前提として、静的に考えることを忘れないようにします!
つまり、「○○(イベント)が発生したら、△△を行う」と考えないようにします。

まず最初に画面に要素を追加しておきましょう。
これについては解説不要でしょう。

次に、この追加したチェックボックスたち(以降「サービス」と呼びます)には、どんな状態が存在する可能性があるのか、考えます。
1つ1つについては、「活性」と「非活性(チェックなし)」でしょう。
サービス全体としては、「Guest」と「Login」のパターンが考えられます。
なので、これらを状態定義・判定しておけばよいはずです。

また忘れてはならないのは、他への影響です。
サービスが影響を受けるもの(ここではラジオボタンですね)については、サービスの判定メソッドの実装で吸収しています。
問題はサービスに影響を受けるものを変更しなければならないことです!!
さて、今回の改造ではOKボタンが新たにサービスの影響を受けることになりました。
つまり、OKボタンの判定メソッドに改修が入ることになります。
サービス全体で1つでもチェックが入っているかどうかを、判定材料に追加することになります。

最後に、サービス全体の状態を定義したり、サービス全体で選択されたものがあるかどうかを取得しなければならないことより、サービス全体に対するインスタンスを生成しておくと理想的でしょう。


機能追加の例(解答例)

  • ファイル名は以下のとおりとします。
    ※前回までファイル名を決めていませんでした。
    HTMLファイル
    sample.html
    JSファイル(クラス)
    ElementManager.js
    JSファイル(状態定義・判定)
    DefineState.js
    JSファイル(イベント実装)
    InitEvents.js
  • HTMLファイル

    ※scriptファイルの読み込みを加えてあります。

    チェックボックスを5つ加えました。(見た目は少々カッコ悪くなりましたが)


    <html>
    <head>
      <script type="text/javascript" charset="Shift_JIS" src="ElementManager.js"></script>
      <script type="text/javascript" charset="Shift_JIS" src="DefineState.js"></script>
      <script type="text/javascript" charset="Shift_JIS" src="InitEvents.js"></script>
    <title>サンプルプログラム</title>
    </head>
    <body>
    <table cellspacing="0" style="background-color:#CCCCCC;border-style:outset;border-width:4px;">
      <thead>
        <tr>
          <td>
            <input type="radio" id="UserType_Guest" name="UserType" value="Guest" />
            <label for="UserType_Guest">Guest</label>
          </td>
          <td>
            <input type="radio" id="UserType_Login" name="UserType" value="Login" checked="checked" />
            <label for="UserType_Login">Login</label>
          </td>
        </tr>
      </thead>
      <tbody>
        <tr>
          <td>
            Username:
          </td>
          <td>
            <input type="text" id="UserName" name="UserName" size="16" value="" style="width:8em;" />
          </td>
        </tr>
        <tr>
          <td>
            Password:
          </td>
          <td>
            <input type="password" id="Password" name="Password" size="16" value="" style="width:8em;" />
          </td>
        </tr>
      </tbody>
      <tfoot>
        <tr>
          <td>
            service:
          </td>
          <td>
            <input type="checkbox" name="service" value="News" id="service_News" /><label for="service_News">News</label>
            <input type="checkbox" name="service" value="BBS" id="service_BBS" /><label for="service_BBS">BBS</label>
            <br />
            <input type="checkbox" name="service" value="RSS" id="service_RSS" /><label for="service_RSS">RSS</label>
            <input type="checkbox" name="service" value="Mail" id="service_Mail" /><label for="service_Mail">Mail</label>
            <br />
            <input type="checkbox" name="service" value="Schedule" id="service_Schedule" /><label for="service_Schedule">Schedule</label>
          </td>
        </tr>

        <tr>
          <td>
            <input type="submit" id="Submit" name="Submit" value="OK" style="width:8em;" />
          </td>
          <td>
            <input type="button" id="Cancel" name="Cancel" value="Cancel" style="width:8em;" />
          </td>
        </tr>
      </tfoot>
    </table>
    </body>
    </html>
  • JSファイル(クラス)

    ⇒変更ありません。
  • JSファイル(状態定義・判定)

    ※活性,非活性のメソッドは共通化してあります。(前回までに言及)

    個々のサービス(checkbox)についてインスタンス定義と状態定義を行いました。

    サービスをまとめたインスタンスを作成し、状態定義と判定メソッドを実装しました。

    サービスをまとめたインスタンスに、isSelectedメソッドを追加しました。

    OKボタンの判定メソッドに、サービスをまとめたもののisSelected()の結果を考慮に入れました。


    (function() {



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



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



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



    //--------------------------------------
    // ラジオ - 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", setDisabled_true);



    // 状態 "Usable" は活性状態
    textUserName.defineState("Usable", setDisabled_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", setDisabled_true);



    // 状態 "Usable" は活性状態
    textPassword.defineState("Usable", setDisabled_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;
    };



    //--------------------------------------
    // 個々のサービス
    var serviceN = ElementManager.createInstance("ServiceN", "service_News");
    var serviceB = ElementManager.createInstance("ServiceB", "service_BBS");
    var serviceR = ElementManager.createInstance("ServiceR", "service_RSS");
    var serviceM = ElementManager.createInstance("ServiceM", "service_Mail");
    var serviceS = ElementManager.createInstance("ServiceS", "service_Schedule");



    // 状態 "Disabled" は非活性状態
    serviceN.defineState("Disabled", setDisabled_true);
    serviceB.defineState("Disabled", setDisabled_true);
    serviceR.defineState("Disabled", setDisabled_true);
    serviceM.defineState("Disabled", setDisabled_true);
    serviceS.defineState("Disabled", setDisabled_true);



    // 状態 "Usable" は活性状態
    serviceN.defineState("Usable", setDisabled_false);
    serviceB.defineState("Usable", setDisabled_false);
    serviceR.defineState("Usable", setDisabled_false);
    serviceM.defineState("Usable", setDisabled_false);
    serviceS.defineState("Usable", setDisabled_false);



    //--------------------------------------
    // サービスをまとめたもの
    var services = ElementManager.createInstance("Services", null);



    // 状態 "Guest"
    services.defineState("Guest", function() {
      ElementManager.getInstance("ServiceN").applyState("Usable");
      ElementManager.getInstance("ServiceB").applyState("Usable");
      ElementManager.getInstance("ServiceR").applyState("Disabled");
      ElementManager.getInstance("ServiceM").applyState("Disabled");
      ElementManager.getInstance("ServiceS").applyState("Disabled");
      var objR = ElementManager.getInstance("ServiceR").getElement();
      if(objR && objR.checked) {
        objR.checked = false;
      }
      var objM = ElementManager.getInstance("ServiceM").getElement();
      if(objM && objM.checked) {
        objM.checked = false;
      }
      var objS = ElementManager.getInstance("ServiceS").getElement();
      if(objS && objS.checked) {
        objS.checked = false;
      }
    });



    // 状態 "Login"
    services.defineState("Login", function() {
      ElementManager.getInstance("ServiceN").applyState("Usable");
      ElementManager.getInstance("ServiceB").applyState("Usable");
      ElementManager.getInstance("ServiceR").applyState("Usable");
      ElementManager.getInstance("ServiceM").applyState("Usable");
      ElementManager.getInstance("ServiceS").applyState("Usable");
    });



    // 状態判定
    services.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 state = null;
      if(rgo && rgo.checked) {
        state = "Guest";
      } else if(rlo && rlo.checked) {
        state = "Login";
      }
      return state;
    };



    // サービスが1つでもチェックされているかどうかを取得する
    services.isSelected = function() {
      var isSelected = false;
      var objN = ElementManager.getInstance("ServiceN").getElement();
      var objB = ElementManager.getInstance("ServiceB").getElement();
      var objR = ElementManager.getInstance("ServiceR").getElement();
      var objM = ElementManager.getInstance("ServiceM").getElement();
      var objS = ElementManager.getInstance("ServiceS").getElement();
      if)((objN && objN.checked) || (objB && objB.checked) || (objR && objR.checked) || (objM && objM.checked) || (objS && objS.checked))( {
        isSelected = true;
      }
      return isSelected;
    };



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



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



    // 状態 "Usable" は活性状態
    buttonOk.defineState("Usable", setDisabled_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 svc = ElementManager.getInstance("Services");
      var state = null;

      if(rgo && rlo && uno && pso && svc) {
        if(rgo.checked && svc.isSelected()) {
          state = "Usable";
        } else if(rlo.checked && (uno.value.length > 0) && (pso.value.length > 0) && svc.isSelected()) {
          state = "Usable";
        } else {
          state = "Disabled";
        }
      } else {
        state = "Disabled";
      }
      return state;
    };



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



    })();
  • JSファイル(イベント実装)

    各サービスにイベント(OKボタンの再考慮)を実装しました。

    ラジオクリックでサービスまとめて再考慮するようにしました。

    初期化時にサービスまとめて再考慮します。


    (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 sNe = ElementManager.getInstance("ServiceN");
      var sNo = sNe ? sNe.getElement() : null;
      var sBe = ElementManager.getInstance("ServiceB");
      var sBo = sBe ? sBe.getElement() : null;
      var sRe = ElementManager.getInstance("ServiceR");
      var sRo = sRe ? sRe.getElement() : null;
      var sMe = ElementManager.getInstance("ServiceM");
      var sMo = sMe ? sMe.getElement() : null;
      var sSe = ElementManager.getInstance("ServiceS");
      var sSo = sSe ? sSe.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 svc = ElementManager.getInstance("Services");
        svc.applyState(svc.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;
      }
      // チェックボックス変更
      sNo.onclick = function() {
        var boe = ElementManager.getInstance("OkButton");
        boe.applyState(boe.judgeState());
      };
      sBo.onclick = sNo.onclick;    
      sRo.onclick = sNo.onclick;    
      sMo.onclick = sNo.onclick;    
      sSo.onclick = sNo.onclick;    

    }



    //--------------------------------------
    // オンロード
    this.onload = function() {
      // オンロード実行
      var une = ElementManager.getInstance("UserName");
      une.applyState(une.judgeState());
      var pse = ElementManager.getInstance("Password");
      pse.applyState(pse.judgeState());
      var svc = ElementManager.getInstance("Services");
      svc.applyState(svc.judgeState());

      var boe = ElementManager.getInstance("OkButton");
      boe.applyState(boe.judgeState());
      // イベント実装
      attachEvent();
    };



    })();


機能追加における考え方・影響について

ここで例に挙げているクラスでの“静的に考える”とは、
「(それぞれの注目要素について)どんな状態があり得るか」を列挙して定義し、
「(注目要素以外が)どんな状況ならば、どの定義にしなければならないか」の判定と
「各イベントで、どの要素の状態についてどんな順序で再考慮しなければならないか」
を考えることになります。
イベントから順に追って考えるのではなく、状態・条件・判定などを準備して最後に「どのイベントで再考慮が発生するか」を考えます。
これにより、驚くほどバグ(「あっ、ここの要素が非活性のままだ」などという類のもの)が減り、ソースコードが(意味的に)透明化することでしょう。
(実際にこの例を考えてみて、“どこをどう修正すればよいか”が明確になりやすいと思いませんか?)


次回予告

静的状態管理の利点・欠点についてまとめたいと思います。