Observerパターンについて考える
Observerパターン、名前は知ってるけど、いまいち使いどころがよく分かりませんでした。
「状態の変化を通知する仕組み」とのことですが、あまりそういう状況に出くわさないというか、頭にピンとこないというか…。
調べてみたんですが、おそらく自分は勘違いしてました。
これ、別に通知とか関係ないのでは。
結局のところ、
<?php $objects = [ new ObjectA(), new ObjectB(), new ObjectC() ]; foreach ($objects as $object) { $object->commonMethod(); }
上記のように、「同名のメソッドを持つなら、ループできるよ」というだけのように思います。
<?php interface Object { public function commonMethod(); }
インターフェースを作って、共通のメソッドを持たせます。
そしてもう一点、「状態変化をどのObserverに通知するか(共通メソッドをどのオブジェクトに実行させるか)」ですが、新人の頃の自分なら、わざわざフラグ用の変数を作っていたように思います。
<?php $objectA->isObserving = true; $objectB->isObserving = false; $objectC->isObserving = true; $objects = [$objectA, $objectB, $objectC]; foreach ($objects as $object) { if ($object->isObserving) { $object->commonMethod(); } }
でもそうじゃなく、Subject(メインルーチン)にObserver(メソッドを実行させるオブジェクト)を配列で持たせてやれば、それでいいんですよね。
<?php $subject->observerList = [ $ObjectA, $ObjectC ]; foreach ($subject->observerList as $object) { $object->commonMethod(); }
フラグ用の変数を作るのは、今後控えようと思います。フラグを立てたいオブジェクトを配列に持たせてやれば、それでよい!
結局、JavaScriptはどう継承すればいいのか
結局、JavaScriptはどう継承すればいいのか。
調べてもサイトによって書いてる事が違うので、ひとまず、信用できそうな方法を以下にまとめます。
(非 ES6 です。)
乃木坂46の秋元真夏を親クラス、相楽伊織を子クラスとします。
2期生の相楽は、1期生秋元の意思を継いでるわけですね。
prototype継承用の関数。
一時的なプロキシコンストラクタを使います。
// prototype継承用の関数 var inherit = (function() { var F = function() {}; // クロージャを利用 return function(C, P) { F.prototype = P.prototype; C.prototype = new F(); C.uber = P.prototype; C.prototype.constructor = C; } }());
こちら、親クラス。
// 親クラス var Parent = function() { this.group = '乃木坂46'; this.name = '秋元真夏'; } Parent.prototype.shoot = function() { console.log(this.group + 'の' + this.name + 'です。ズッキュン!'); }
子クラスです。
applyメソッドで、親コンストラクタのプロパティを継承。
inherit関数で、親のprototypeに設定されたメソッドを継承してます。
// 子クラス var Child = function() { Parent.apply(this, arguments); // 親のプロパティを継承 this.name = '相楽伊織'; } inherit(Child, Parent); // 親のメソッドを継承
実行。相楽伊織がズッキュンを継承してくれました。
var child = new Child; child.shoot(); // 乃木坂46の相楽伊織です。ズッキュン!
うーん、まだprototypeを使いこなしてる感じがしないなぁ。
もう少し粘着して覚えていきます。
デコレータパターンでテンプレートメソッドを使う(PHP)
うーん、今までデコレータパターンを勘違いしてた。
デコレータパターンは継承と書き換え可能でした。
デコレータは動的に継承っぽい関係を作れるので、その点メリットがあります。
ただ、どの程度書き換え可能なのか…?
自分はテンプレートメソッドをよく使うんですが、そういうこともデコレータは出来るのか…?
というわけで、デコレータパターンでテンプレートメソッドを再現してみました。
ひとまず、一旦、普通の継承を。
<?php class Super { public function render() { echo '<p>2</p>'; $this->tmplMethod(); echo '<p>4</p>'; } } class Child extends Super { public function render() { echo '<p>1</p>'; parent::render(); echo '<p>5</p>'; } public function tmplMethod() { echo '<p>3</p>'; } } $obj = new Child(); $obj->render();
こいつを、デコレータで書き換える。
親となるクラス。
<?php class Super { public function render(callable $callback) { echo '<p>2</p>'; $callback(); echo '<p>4</p>'; } }
子供のクラス。
<?php class Child { public function __construct(Super $super) { $this->super = $super; } public function render() { echo '<p>1</p>'; $this->super->render([$this, 'tmplMethod']); echo '<p>5</p>'; } public function tmplMethod() { echo '<p>3</p>'; } }
実行。
<?php $obj = new Child(new Super); $obj->render();
うーん、引数でコールバックを渡すくらいしか、思いつかないなぁ。
ひとまず、この方法で使っていきます。
JavaScriptを超シンプルなMVCで書く(2)
以前の記事「JavaScriptを超シンプルなMVCで書く」を見返すとイマイチなので書き直し。
lonely-programmer.hatenablog.jp
以下挙動のアプリケーションを作ります。
・ボタンをクリックする毎に、白石麻衣の所属するグループが、乃木坂46 / 欅坂46 / SKE46 の順番でループする。
まずモデル。
var model = (function() { /* privateメンバ */ var count = 0; var groups = []; // 現在のグループを取得 var getNowGroup = function() { return groups[getIndex()]; }; // 現在のインデックスを取得 var getIndex = function() { // 0 → 1 → 2 → 0 → 1 → 2... とインデックスをループ return count % groups.length }; /* publicメンバ */ // グループを追加 var addGroup = function(group) { groups.push(group); }; // グループを変更 var changeHerGroup = function() { count++; $(this).trigger('groupIsChanged'); }; // グループ名を表示 var getGroupName = function () { return getNowGroup().getName(); }; // publicメンバを開示 return { addGroup: addGroup, changeHerGroup: changeHerGroup, getGroupName: getGroupName }; })();
モジュールパターンでオブジェクトを返却してます。
このオブジェクトはクロージャです。
privateメンバには外部からアクセスできません。
注目すべきはここ。
var changeHerGroup = function() { count++; $(this).trigger('groupIsChanged'); };
モデル内の値に変化があった場合、'groupIsChanged'イベントが発火されます。
オブジェクトではなく、コンストラクタ関数で返却する場合は、メソッド後半を以下のように書き変えます。
// コンストラクタ関数にpublicメンバを登録 var Constr = function() { this.addGroup = addGroup; this.changeHerGroup = changeHerGroup; }; // プロトタイプにpublicメンバを登録 Constr.prototype = { getGroupName: getGroupName }; // コンストラクタ関数を返却 return Constr;
グループ用のオブジェクトをモデルのプロパティに追加。
変数nameはprivateです。
// グループ用のコンストラクタ関数 function Group(name) { var name = name; // 書かなくても良い this.getName = function() { return name; }; } // グループ追加 model.addGroup(new Group('乃木坂46')); model.addGroup(new Group('欅坂46')); model.addGroup(new Group('SKE48'));
ボタンがクリックされると、モデル内の値を更新。
// ボタンクリック時のイベントハンドラ $('button').on('click', function() { model.changeHerGroup(); });
カスタムイベントを作成。
// レンダリング用カスタムイベント $('span').on('render', function() { $(this).text(model.getGroupName()); });
モデル内の値に変化があれば、再レンダリングを行います。
jQueryのon()メソッドは、DOMオブジェクト以外も監視できることを最近知りました…。
// モデルを監視 $(model).on('groupIsChanged', function() { // テキストを再レンダリング $('span').trigger('render'); });
こちら、初期表示。
// テキストを初期表示 $('span').trigger('render'); // JavaScript処理完了後に画面表示 $('body').css('display', 'block');
ES6のことは一切考慮してません。笑
次回以降の記事で、ES6に書き換えてみようと思います。
以下、全コード。
<html> <script src="jquery.js"></script> <script> $(function() { var model = (function() { /* privateメンバ */ var count = 0; var groups = []; // 現在のグループを取得 var getNowGroup = function() { return groups[getIndex()]; }; // 現在のインデックスを取得 var getIndex = function() { // 0 → 1 → 2 → 0 → 1 → 2... とインデックスをループ return count % groups.length }; /* publicメンバ */ // グループを追加 var addGroup = function(group) { groups.push(group); }; // グループを変更 var changeHerGroup = function() { count++; $(this).trigger('groupIsChanged'); }; // グループ名を表示 var getGroupName = function () { return getNowGroup().getName(); }; // publicメンバを開示 return { addGroup: addGroup, changeHerGroup: changeHerGroup, getGroupName: getGroupName }; })(); // Controller (function() { // グループ用のコンストラクタ関数 function Group(name) { var name = name; // 書かなくても良い this.getName = function() { return name; }; } // グループ追加 model.addGroup(new Group('乃木坂46')); model.addGroup(new Group('欅坂46')); model.addGroup(new Group('SKE48')); // ボタンクリック時のイベントハンドラ $('button').on('click', function() { model.changeHerGroup(); }); })(); // View (function() { // モデルを監視 $(model).on('groupIsChanged', function() { // テキストを再レンダリング $('span').trigger('render'); }); // レンダリング用カスタムイベント $('span').on('render', function() { $(this).text(model.getGroupName()); }); // テキストを初期表示 $('span').trigger('render'); // JavaScript処理完了後に画面表示 $('body').css('display', 'block'); })(); }); </script> <body style="display: none"> <button>グループ変更</button> <p><span></span>の白石麻衣です。</p> </body> </html>
Riot.jsでモデルっぽいものを書いてみる
Riot.jsを初めて触ってみた。
あまりにシンプル過ぎて驚いた!
ほとんど覚えること無しに、それなりに書けそう。
というわけで、前回・前々回の流れで、簡単なサンプルをRiot.jsで書いてみます。
悩んだのが、Riot.jsにはモデルが見当たらない…?
おそらく、Observableがそれにあたるのか…?
とりあえず、モデルっぽいものを使ってみることにします。
HTMLはこれだけ。
<html> <body> <div id="nogi"></div> <div id="nogi-text"></div> <script src='https://cdnjs.cloudflare.com/ajax/libs/riot/2.3.18/riot+compiler.js'></script> </body> </html>
モデルの発火、監視を可能にする。
<script> var model = { isNogi: true } riot.observable(model); </script>
これが、カスタムタグ。
HTMLタグと、JavaScriptとが合体してるイメージ。スタイルシートも定義できます。
「このタグはこういう挙動をする」ってのが見て一発で分かる。
わざわざソースコードを追いかけ回す必要もない。便利だなぁ…。
<script type='riot/tag'> <nogi> <button onclick='{ invert }'>{ name }</button> // モデルを更新 this.invert = function () { model.isNogi = !model.isNogi; // 通知イベント発火 model.trigger('hasInverted'); }; // 通知イベントを検知して、要素を描画 model.on('hasInverted', (function () { this.render(); }).bind(this)); this.render = function () { this.name = model.isNogi ? '乃木坂46' : '欅坂46'; }; // 初期描画 this.render(); </nogi> </script>
こちら、別のカスタムタグ。
モデルの発火を検知して、再レンダリングする。
<nogi-text> <p>{ description }</p> // 通知イベントを検知して、要素を描画 model.on('hasInverted', (function () { this.render(); }).bind(this)); this.render = function () { this.description = model.isNogi ? '2011年に結成されました。' : '2015年に結成されました。'; this.update(); }; // 初期描画 this.render(); </nogi-text>
マウントする。マウントって何?
カスタムタグと関連付けます。
<script> // マウント riot.mount('#nogi', 'nogi'); riot.mount('#nogi-text', 'nogi-text'); </script>
覚えるのも書くのも簡単そうなので、せめて公式ドキュメントは全部読んでおこうと思います。
低コストで最新のJavaScriptフレームワークを身につけられる。お得ですなぁ…。
Backbone.jsでCollectionを触ってみる
Collectionを触ったことが無かったので、簡単に触っておこうと思います。
Cllectionは複数のModelを突っ込める器のようなものです。
DBに例えると、Modelがレコードだとすると、Collectionはテーブルになります。
以下、コードの全容。
タスクリストのようなアプリケーションです。
フォームにテキストを入力してボタンを押すと、リストにテキストが追加されます。
ただし、ブラウザの更新ボタンを押すとすべて初期化されます。笑
<html> <script src="underscore-min.js"></script> <script src="jquery.js"></script> <script src="backbone-min.js"></script> <script> $(function() { // モデル var Model = Backbone.Model.extend(); // コレクション var Collection = Backbone.Collection.extend({ model: Model, }); var collection = new Collection; // ビュー var View = Backbone.View.extend({ el: 'body', events: { 'click button': 'addItem', }, initialize: function() { this.listenTo(this.collection, 'add', this.render); }, addItem: function() { this.collection.add({ name: $('input').val() }); }, render: function(model) { $('ul').append($('<li>').text(model.get('name'))); $('input').val(''); }, }); var view = new View({ collection: collection }); }); </script> <body> <input type="text"> <button>追加</button> <ul> </ul> </body> </html>
var Collection = Backbone.Collection.extend({ model: Model, }); var collection = new Collection;
コレクションの作成。使用するモデルを指定します。
var View = Backbone.View.extend({ el: 'body', events: { 'click button': 'addItem', }, initialize: function() { this.listenTo(this.collection, 'add', this.render); }, addItem: function() { this.collection.add({ name: $('input').val() }); }, render: function(model) { $('ul').append($('<li>').text(model.get('name'))); $('input').val(''); }, }); var view = new View({ collection: collection });
ビューの作成。
コレクションにモデルが追加されると、コレクションで「add」イベントが発火されます。
ビューはaddイベントの発火を感知し、renderメソッドが実行されます。
render: function(model) { $('ul').append($('<li>').text(model.get('name'))); $('input').val(''); },
このメソッドですが、自動で引数に追加されたモデルが渡されるようです。こりゃ便利だ。
Backbone.jsいいなぁ。でも古いフレームワークなので、新しいブログ記事が全く出てこない。
JavaScriptフレームワークの歴史を追うってことで、今後はReact.jsとかも触っていきたいです。
Backbone.jsで超シンプルなMVCを書く
前回はピュアなJavaScriptでシンプルなMVCを書くという趣旨でしたが…
「フレームワークを使った方がシンプルに書けるのでは?」
という考えが巡り始めたので、今回は前回の内容をBackbone.jsで書き換えます。
Backbone.jsを簡単に説明すると…
日本語に訳して「背骨」なだけあって、MVCの枠組みだけを提供してくれます。
jQueryと共存できる…というか、Backbone.jsのライブラリ自体がjQueryに依存してます。
要は、大枠はBackbone.jsがやるから、細かいことはjQueryでやってくれってことです。
■ Model
var Model = Backbone.Model.extend({ buttonStatus: true, changeData: function() { if (this.buttonStatus) { this.buttonStatus = false; } else { this.buttonStatus = true; } this.trigger('changingIsDone'); } }); var model = new Model;
まずモデル。データは全部モデルに持たせます。
モデルからDOM操作は行いません。
this.trigger('changingIsDone');
データに変更があれば、モデルの「changingIsDone」イベントが発火されます。
※HTMLの各要素から発火されるイベント(onclick, onchange等)とは別の概念と思われます。
■ View
var View = Backbone.View.extend({ el: 'body', events: { 'click button': 'switchButtonName', }, initialize: function() { this.listenTo(this.model, 'changingIsDone', this.render); this.render(); }, switchButtonName: function() { this.model.changeData(); }, render: function() { var buttonName; if (this.model.buttonStatus) { buttonName = '乃木坂46'; } else { buttonName = '欅坂46'; } $('button').text(buttonName); }, }); var view = new View({ model: model });
events: { 'click button': 'switchButtonName', },
イベントハンドラを登録してます。
switchButtonNameメソッドから、モデルを操作してます。
initialize: function() { this.listenTo(this.model, 'chngingIsDone', this.render); this.render(); },
initializeメソッドはPHPの__construct()のようなものです。
listenToメソッドで、モデルの「chngingIsDone」イベントの監視を開始しました。
「chngingIsDone」イベントが発火すると、自動でrenderメソッドが実行されます。
render: function() { var buttonName; if (this.model.buttonStatus) { buttonName = '乃木坂46'; } else { buttonName = '欅坂46'; } $('button').text(buttonName); },
renderメソッドではDOM操作を行っています。
ここで面白いのは、モデルの「changingIsDone」イベントを監視できるViewを複数追加できることです。
■ View 2
var SubView = Backbone.View.extend({ el: 'p', initialize: function() { this.listenTo(this.model, 'changingIsDone', this.render); this.render(); }, render: function() { var description; if (this.model.buttonStatus) { description = '2011年に結成されました。'; } else { description = '2015年に結成されました。'; } $(this.el).text(description); }, }); var subView = new SubView({ model: model });
こちらのViewもモデルを監視しています。
つまり、モデルに変化があれば、View, SubViewの両方がレンダリングされる…ということです。
<body> <button></button> <p></p> </body>
こちら、HTML。
ピュアなJavaScriptより、こちらの方がシンプルに書けました。
デメリットは、そのフレームワークの経験者でないと、システムの修正を気軽にできないことでしょうか…。