はじめに
最近、O'Reillyの『JavaScript パターン』を読みました。
とても良い本なので、JavaScriptを使っていて、まだ読んでない方は是非読んでみてください。
さて、この本の「7章 デザインパターン」の「7.9 オブザーバー」のコーディング(リンク)が、1章から順に本を読んできた身としては、今ひとつすっきりしません。普段、JavaでObserverパターンをコーディングしているというのも理由かもしれません。整理のため、「4章 関数」、「5章 オブジェクト生成のパターン」のテクニックを使ってJavaScriptでObserverパターンをコーディングしてみました。
モデル
概要は、Wikiの「Observerパターン」に準拠するモデルになります。
ただ、Wikiの説明は、JavaやC++のような静的型付け言語を前提としたインターフェースを用いた説明になっているため、JavaScriptのような動的型付け言語にそのまま当てはめることができません。JavaScriptでは、インターフェースという静的型付け言語の機能を使用せずにコーディングする必要があります。
そこで、今回は、JavaScriptが動的型付け言語であることを生かしてダッグタイピング(duck typing)でコーディングしていきます。具体的には、Observerインターフェースは使用せず、各Observerクラスがイベントのインターフェースとなるnotify()メソッドを直接コーディングします。Subjectクラス側では、apply()メソッドを使って登録されている各Observerのインスタンスのnotify()メソッドを呼び出します。
また、WikiではSubjectはインターフェースですが、今回は、Subjectを実体クラスとしてコーディングします。こちらの変更は、静的型付け言語、動的型付け言語とは関係なく、簡単のための変更になります。
-
Subject
- オブザーバーを管理するクラスです。
- オブザーバーを登録するメソッド
add()
をもっています。 - オブザーバーを解除するメソッド
remove()
をもっています。 - 登録されている全てのオブザーバーにイベントを通知するメソッド
notifyAll()
をもっています。
-
ObserverA
- オブザーバークラスです。
-
notify()
メソッドでイベントを受信したら"ObserverA"をコンソール出力します。
-
ObserverB
- オブザーバークラスです。
-
notify()
メソッドでイベントを受信したら"ObserverB"をコンソール出力します。
ソースコード
ディレクトリ構成は、以下のようになります。
QiitaObserver/
├ package.json
└ src/
├ main.js
├ observer_a.js
├ observer_b.js
└ subject.js
グローバル汚染しないように、即時関数で囲んでいます。
main.js
は、Subjectや各種Observerのコントローラーです。
(function () {
"use strict";
var Subject = require('./subject');
var ObserverA = require('./observer_a');
var ObserverB = require('./observer_b');
var subject = new Subject();
var observerA = new ObserverA();
var observerB = new ObserverB();
console.log("### test 1 ###");
subject.add(observerA);
subject.notifyAll();
console.log("### test 2 ###");
subject.add(observerB);
subject.notifyAll();
console.log("### test 3 ###");
subject.remove(observerA);
subject.notifyAll();
}());
/**
* ObserverA
*
* @namespace observer_a
* @exports ObserverA
*/
module.exports = (function () {
"use strict";
/**
* ObserverAのコンストラクタ
*
* @constructor
* @name ObserverA
*/
var ObserverA = function () {
};
ObserverA.prototype = {
/**
* 通知を受け取るメソッド
*
* @method
* @name notify
*/
notify : function() {
console.log("ObserverA");
}
};
return ObserverA;
}());
/**
* ObserverB
*
* @namespace observer_b
* @exports ObserverB
*/
module.exports = (function () {
"use strict";
/**
* ObserverBのコンストラクタ
*
* @constructor
* @name ObserverB
*/
var ObserverB = function () {
};
ObserverB.prototype = {
/**
* 通知を受け取るメソッド
*
* @method
* @name notify
*/
notify : function() {
console.log("ObserverB");
}
};
return ObserverB;
}());
/**
* オブザーバーの管理と変更通知を行います
*
* このクラスに登録するオブザーバーはnotify()を実装してください
*
* @namespace subject
* @exports Subject
*/
module.exports = (function () {
"use strict";
/**
* Subjectのコンストラクタ
*
* @constructor
* @name Subject
*/
var Subject = function () {
/**
* オブザーバーを格納するリスト
*
* @name handlers
* @return {object}
*/
this.handlers = [];
};
Subject.prototype = {
/**
* オブザーバーをリストに登録する
*
* @method
* @name add
* @param {type} obj 追加するオブザーバー
*/
add: function (obj) {
this.handlers.push(obj);
},
/**
* オブザーバーをリストから削除する
*
* @method
* @name remove
* @param {type} obj 削除したいオブザーバー
* @returns {boolean} true: 削除した、false: 削除しなかった
*/
remove: function (obj) {
for (var i = 0, len = this.handlers.length; i < len; i++) {
if (this.handlers[i] === obj) {
this.handlers.splice(i, 1);
return true;
}
}
return false;
},
/**
* リストに登録されているオブザーバーに通知する
*
* @method
* @name notifyAll
*/
notifyAll: function () {
var args = Array.prototype.slice.call(arguments, 0);
for (var i = 0, len = this.handlers.length; i < len; i++) {
var handler = this.handlers[i];
handler.notify.apply(handler, args);
}
}
};
return Subject;
}());
{
"name": "QiitaObserver",
"version": "0.1.0",
"author": "olympic2020",
"keywords": ["observer pattern"],
"dependencies": {},
"scripts": {
"start": "node src/main.js"
}
}
実行方法
QiitaObserverディレクトリで実行します。
node.jsが必要です。
$ node src/main.js
npmがインストールされていれば、以下でOKです。
$ npm start
実行結果は以下のようになります。
### test 1 ###
ObserverA
### test 2 ###
ObserverA
ObserverB
### test 3 ###
ObserverB
- test 1
ObserverAをSubjectに登録したので「ObserverA」が出力されます。 - test 2
さらにObserverBをSubjectに登録したので「ObserverA」と「ObserverB」が出力されます。 - test 3
ObserverAをSubjectから削除したので「ObserverB」のみが出力されます。
おわりに
Observerパターンを意識しない場合、通常、コールバックを使って似たようなことを行っていると思います。ただ、気づけばfunctionのネストが深いプログラムになってしまい、後でテストやデバッグで泣きをみることになっているのではないでしょうか。
今回、JavaScriptでObserverパターンをコーディングしてみましたが、当然のことながら、JavaScriptでも、Observerパターンを使うことで、Javaでコーディングしたときと同じように、functionのネストが浅くなり、見通しのよいコードになることが分かりました。
参考
こちらを参考にさせていただきました。
【まとめ】JavaScriptでデザインパターン