状態管理 概論

なんで今回やるの?

  • Webはすっきりかけるのに、JSだと辛くなる
  • 何が違うのか?
  • 乗り越える方法は?

よくある光景

  • テレビのチャンネルは変えられるのに
    • ビデオの予約が出来ない
  • 自販機でジュースは買えるのに
    • 新幹線の券売機で切符が買えない

鍵は状態管理

  • テレビのチャンネル : ステートレス
  • ビデオの予約 : ステートフル
  • 自販機 : 簡単な状態
  • 新幹線の券売機 : 複雑な状態

状態は複雑さの元

  • ステートレスが理想
  • 状態を整理してシンプルに保つ
  • 状態をきれいに書く

1.1 GUIプログラミングの歴史

  • 1990〜 クラサバ
  • 2000〜 Web3層
  • 2004〜 Ajax/RIA

※年代は適当 ※クラサバ : Client-Server

クラサバ以前

  • AccessとかFileMakerなどの1台構成
  • ダム端末(シンクライアント)と中央コンピューター

クラサバ

  • DBサーバー:DB(SQL)
    • →ステートレス
  • クライアント:業務ロジック、GUIコード(VB)
    • ステートフル

データ処理の負荷が分散して作りやすいが、クライアントにGUIコード以外の業務ロジックが集中。ステートフルGUIプログラミングの発展。

Web3層

  • DBサーバー:DB(SQL)
    • →ステートレス
  • アプリケーションサーバー:業務ロジック、Webアクション
    • →ステートレス
  • クライアント:静的HTML
    • →ステートレス

全部ステートレス!シンプルすぎて使いにくい。ステートフルGUIプログラミングの空白期。

Ajax/RIA(←いまココ)

  • DBサーバー:DB(SQL)
    • →ステートレス
  • アプリケーションサーバー:業務ロジック、Webアクション
    • →ステートレス
  • クライアント:動的HTML/JavaScript
    • ステートフル

業務ロジックはシンプルに、UIはリッチに。GUIプログラミングの復活。

→ステートフル(状態)の制御がクライアントプログラミングの鍵。

1.2 なせGUIプログラミングが難しいのか?

  • イベントドリブン
    • 直感的にコードが書ける
    • ユーザーの操作が動作の起点
    • → 制御が難しい
    • イベントドリブンプログラムになるので、そのままだと変数のスコープがとぎれる
    • → グローバル変数に頼ったり、状態を管理する変数がばらばらになる
  • MVC
    • Modelで状態を管理する変数スコープをまとめる
    • 状態の変更を正しくViewに通知する
    • 設計が難しい
  • Morph
    • GUI部品の徹底的な可視化
    • → 構築、デバッグが楽
    • 通知ではなく監視
    • → 疎結合
    • 遅い、GUIの総取り替え

→そもそもGUIは本質的に難しく、まだまだ研究が続いているような状況。設計手法・実装技術をちゃんと学ばないと辛い。

状態→見えるようにする

  • GUIはステートフルなので状態の管理が必須
  • そもそも「状態」を意識したことがない人が多いかも

そこで、

  1. 状態を抽出して見えるようにする実習
  2. 状態をうまく扱う設計と実装方法を実習

並び替え可能リストをステートパターンで作ってみる

目標

以下のようなアイテムをドラッグ&ドロップで並び替えることができるリストを作成します。

  • Item-1
  • Item-2
  • Item-3

Step1.ステートなしで実現してみる

Step1-1.元になるHTML/CSS

<html>
<head>
<style>
  #list li {
    cursor: pointer;
    font-size: 20px;
  }
  #drag-item {
    background:yellow;
    alpha: 0.5;
    z-index: 99;
    position: absolute;
    font-size: 20px;
  }
   .drag-over {
    border-bottom: 2px dashed blue;
    background: yellow;
  }
</style>
<script src="../lib/prototype.js"></script>
<script>
</script>
</head>
<body onselectstart="return false;"
      onmousedown="if (typeof event.preventDefault != 'undefined') { event.preventDefault(); }">
<ul id="list">
  <li>Item-1</li>
  <li>Item-2</li>
  <li>Item-3</li>
  <li>Item-4</li>
  <li>Item-5</li>
</ul>
<span id="drag-item">test</span>
Debug : <span id="debug"></span>
</body>
</html>

Step1-2.選択処理を組み込み

<script>
var selectedObject;
var ulElem;
var selected = false;

Event.observe(window, 'load', function(event) {
  ulElem = $('list');
  this.selectedObject = null;
  Element.hide("drag-item");
  Event.observe(ulElem, 'mousedown', function(event) {
     if (selected == false) {
       var target = Event.element(event);
       if (target.tagName == "LI") {
         selectedObject = target;
         $("drag-item").textContent = selectedObject.textContent;
         Element.show("drag-item");
         selected = true;
       }
     }
  });
});
</script>

Step1-3.ドラッグ処理を組み込み

  ...
  Event.observe(ulElem, 'mousemove', function(event) {
    if (selected == true) {
      moveToolTip(event, $("drag-item"));
      var target = Event.element(event);
      if (target.tagName == "LI" && target !== selectedObject) {
        removeClassName(ulElem.childElements(), "drag-over");
        Element.addClassName(target, "drag-over");
      }
    }
  });
  ...

function moveToolTip(event, toolTipElem) {
  var xx = Event.pointerX(event);
  var yy = Event.pointerY(event);
  toolTipElem.style.left = xx + 2;
  toolTipElem.style.top = yy + 2;
}

function removeClassName(items, className) {
  for (var i = 0; i < items.length; i++) {
    var item = items[i];
    Element.removeClassName(item, className);
  }
}
...

Step1-4.移動処理を組み込み

...
  Event.observe(ulElem, 'mouseup', function(event) {
    if (selected == true) {
      removeClassName(ulElem.childElements(), "drag-over");
      var target = Event.element(event);
      if (target.tagName == "LI" && target !== selectedObject) {
        moveElement(target, selectedObject);
      }
      Element.hide("drag-item");
      selectedObject = null;
      selected = false;
    }
  });
...
function moveElement(afterElement, target) {
  Element.remove(target);
  Insertion.After(afterElement, target);
}

Step2.状態を見つける

.Step2-1.状態遷移表を使って、状態を見つけ出してみましょう。

Step3.ステートパターンを利用して、書き換えてみる (完成版)

Step3-1.ステートの雛形

<script>
var state;
var selectedObject;
var ulElem;

Event.observe(window, 'load', function(event) {
  ulElem = $('list');
  transitState(new NormalState());
  Event.observe(ulElem, 'mousedown', function(event) {
    state.onMouseDown(event);
  });
  Event.observe(ulElem, 'mousemove', function(event) {
    state.onMouseMove(event);
  });
  Event.observe(ulElem, 'mouseup', function(event) {
    state.onMouseUp(event);
  });
});

function transitState(state) {
  $("debug").textContent = this.state + " -> " + state;
  this.state = state;
}

var AbstractState = Class.create();
AbstractState.prototype = {
  initialize: function() {
  },
  onMouseDown: function(event) {
  },
  onMouseMove: function(event) {
  }, 
  onMouseUp: function(event) {
  }, 
  toString: function() {
    return "AbstractState";
  }
};

var NormalState = Class.create(AbstractState, {
  initialize: function($super) {
    this.selectedObject = null;
    Element.hide("drag-item");
  },
  onMouseDown: function(event) {
    var target = Event.element(event);
    if (target.tagName == "LI") {
       // (1) 次の状態へ遷移
    }
  },
  toString: function() {
    return "NormalState";
  }
});
...
</script>

Step3-2.ドラッグ

Thinking...

Step3-2.移動

Thinking...

Step4.機能追加

リストの内容を変更できる機能を追加してみましょう。

仕様

  • リストのアイテムをダブルクリック→テキストフィールド表示
  • 編集中にenter keyを押したら編集終了→テキストフィールド非表示、変更内容を反映

Step4-1.追加される状態とイベントに注目して、状態遷移表を追記してみましょう。

Step4-2.実装を追加してみましょう。

ヒント

  • ダブルクリックのイベント名はprototype.jsでは「dblclick」
  • キー入力のイベント名はprototype.jsでは「keydown」。documentにハンドラをセットするのがポイント
  • エンターキーを判別するのは「event.keyCode」が「13」なら。
  • Element.clonePositionメソッドを使うと、あるエレメントを別のエレメントの座標に重ねることができる。

Step5.コンポーネント化してみる

どのようなリストでも並び替えができるようなコンポーネントを作成してみましょう。

Step5-1.コンポーネント化のためのリファクタリング

処理内の「使用するたびに変更したいポイント」を抽出して、一カ所に固める。

Step5-2.コンポーネントのコンストラクタ内にスコープを移動

Step5-3.コンポーネントの作成処理を追加

changed October 9, 2009