状態管理 概論
なんで今回やるの?
- 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はステートフルなので状態の管理が必須
- そもそも「状態」を意識したことがない人が多いかも
そこで、
- 状態を抽出して見えるようにする実習
- 状態をうまく扱う設計と実装方法を実習
並び替え可能リストをステートパターンで作ってみる
目標
以下のようなアイテムをドラッグ&ドロップで並び替えることができるリストを作成します。
- 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