Maintainable Event Handling
jQuery覚えたぜ!って感じの人がとりあえずコードを書くと、だいたいこんな感じになりますね。ちなみに、これは別にjQueryがどうとかいう話ではなくて、本質的には生DOMでも他のライブラリでも同じことです。
$(function() {
$("#foo").on("click", function(e) {
});
$("#bar").on("click", function(e) {
});
$("#baz").on("click", function(e) {
});
})();
要は、特定のElementにイベントハンドラをくっつけて、イベントハンドラの中にアプリケーションロジックをダラダラっと書くわけですね。
ちなみに、「これがダメ!」って言いたいわけではないです。コードの規模に対して適切な抽象化のレベルってあると思うので、静的なWebページに飾り付けをするだけならこういう書き方でもそこまで泣きをみることはないと思います。小規模なコードしか書く予定がないのに変にクラスベースなオブジェクト指向を頑張ってみたりとか、明確な意図なしに「今はClient Side MVCが流行りなんだぜい!」とかやってしまうと逆に後で自分をうらむことになるでしょう。
で、"Maintainable JavaScript"の著者であるNicholas Zakas先生は「こういうイベントハンドラの中にアプリケーションロジックの書いてあるコードはいくない!」と言います。なぜかといえば、
- 実装したアプリケーションロジックを、イベントハンドラ意外の場所から呼び出したくなるのはままあることだ
- 例えば、ユーザのアクション起因じゃなくてViewをプログラムから更新したい時とか...(あるある)
なので、イベントハンドラとアプリケーションロジックは分離しておくと、未来の自分に感謝されます。
$(function() {
var Application = {
handleClick: function(e) {
this.showPopup(e);
},
showPopup: function(e) {
}
};
$("#foo").on("click", function(e) {
Application.handleClick(e);
});
})();
Zakas先生は「これでもまだいかん!」と言います。なぜなら、上の例のshowPopupという関数がevent objectを引数に取ってしまっているので、このままでは結局他の所から呼べないからですね。
まだ、「ユーザのクリック」という呼び出しコンテキストとアプリケーションロジックが結合してしまっています。
なので、以下のように変更します。
$(function() {
var Application = {
handleClick: function(e) {
this.showPopup(e.clientX, e.clientY);
},
showPopup: function(x, y) {
}
};
$("#foo").on("click", function(e) {
Application.handleClick(e);
});
})();
こうすると、ユーザのクリック以外のタイミングでshowPopupを、任意の座標x, yを引数として呼び出せるようになりますね。
また、showPopupのテストも書きやすくなると思います。「アプリケーションロジックには直接Eventを渡さず、必要なデータのみを渡す」ということですね。
ところで、いちいちイベントの種類ごとにhandleなんとかという関数を用意するのもアレなので、handleEventでまとめてしまうのも場合によってはアリかもしれません。
$(function() {
var Application = {
handleEvent: function(e) {
switch (e.type) {
case "click": this.showPopup(e.clientX, e.clientY); break;
case "scroll": ...
case "drag": ...
}
},
showPopup: function(x, y) {
}
};
$("#foo").on("click", Application);
})();
jQueryが正しく扱ってくれるかどうかまでは知りませんが、handleEventという関数が生えたオブジェクトをイベントハンドラとして指定できるのはDOM Level2のれっきとした仕様です。(http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventListener)
ここまで来れば、さらに再利用性を高めるための抽象化を行うまでは後一歩です。
(function() {
function ModalController(el) {
this.elem = el;
el.on("click", this);
}
ModalController.prototype.handleEvent = function(e) {
switch (e.type) {
case "click": this.showPopup(e.clientX, e.clientY); break;
case "scroll": ...
case "drag": ...
}
};
ModalController.prototype.showPopup = function(x, y) {
};
})();
$(function() {
var controller = new ModalController($("#foo"));
})();
が、現実にこんなことをやり始めた場合は、何らかの既成のフレームワークに乗っかることを考え始めた方がいいと思います。
呼び出しコンテキストとアプリケーションロジックの分離、について
上記の話のキモは、「ユーザのクリックが行われ、それに伴うEvent Objectがある」というコンテキストと、実際のアプリケーションロジックを分離することで再利用性を高めている部分にあります。
これってどこかで聞いた話ですね。Webアプリケーションフレームワークの多くが採用している、MVCパターンに則ってアプリを作る場合でも「ControllerにおけるHTTP Requestにひもづくコンテキスト(例えばContext Objectであったり、HTTP Requestを表現するObjectであったり、Formを表現するObjectであったり)をビジネスロジックのレイヤに持ち込まない、伝播しない」という書き方を、まともな人ならすると思います。そうしておかないと、同じビジネスロジックをbatchやworkerなどの他の場所から再利用できなくなってしまいますからね。MVCというフレームワーク/語彙を使わなくても、この辺の原則はまぁ変わらないんじゃないかなーと思うのですが、先生どうなんでしょう。
iOSやAndroidのネイティブアプリでも、やはりユーザのアクションに紐付いたコンテキストと、アプリケーションロジックは分離するとよさそうですね。JavaScriptでのEvent Handlingに限らず、アプリケーションロジックは複数の場所/コンテキストから呼び出される前提で書くものだぜ!という考え方はコードの再利用性を高めてくれるのではないかなーと思います。