まえおき
デザインパターンに関しては色々言われていますね。
個人的には、「確かに微妙だし全体的に時代遅れっぽいけど基礎教養みたいなもんなので基本情報技術なんぞを受ける暇があるなら一通り目を通しておいてもいんじゃねえの?」っていう立場です。
微妙っぽい感じなものが多いデザインパターンなんですが、それでもちゃんと押さえておいた方がいいよねっていうものの一つがオブザーバーパターンです。
アプリケーション的なプログラミングをしていると絶対に避けて通れないパターンなんですけども、以外と存在も意味も知らずになんとなくとコピペで済ませている人に未だにめっちゃ出会うのでちょっとした危機感だよねっていうのが現状です。
このパターンはみんな大好きMVCの基礎となっている設計手法で、Webとかスマホアプリだとかをちゃんとやるには避けて通れないんだ。一切勉強もせずに聞きかじりの知識で「今さらデザパタの記事とかww」「デザパタ厨わろた」とか人のことを批判しちゃう人は先生嫌いだな?
「よく使うんだから基本的な構造くらいは理解しておいた方がいいよ」と言ってもやらずに済ませる人が多い理由の一つに、多くの解説がジャバかC++で教科書通りに書いてあるからかなと思ったので、もっとシンプルな言語で簡潔に実装した例を示したらもしかしたらもしかするかなと思って頑張って書きました。
このエントリが「なんかややこしそうだから今度やろう」と思っているうちに何年も経ってしまった人の助けになればいいなぁとかおもいます。
なおJavaやC++が悪い言語だということではなく、単純に言語仕様が古くさい*1のと冗長なボイラープレートコードが多すぎるせいで、こういう設計の本質を伝えるコード例には不向きだということです。
今回はPythonで書きました。記述が軽量な言語ならなんでも良かったんですけど、個人的な好みでPythonを使うことに決めました。気が向いたらRubyやJavaScriptでの例も書くかもしれませんが、大したコード量じゃないしPythonのコードはシンプル綺麗最高*2なので今回のコード程度であればPythonを書いたことがなくても何ら問題はないはずです。
例
前段階
ここでは、例としてgetリクエストを送るクラスを貴方が作りたいと思ったとします。そういうことにしてください。
疑似コードみたいなもんなので別に実行する必要はないですが、もし実行してみたい方がいる場合はpip install requests
として外部モジュールを導入しておく必要があります。pipはRubyでいうgemみたいなもんです。
# -*- coding: utf-8 -*- from __future__ import (print_function, division, absolute_import, unicode_literals, ) import requests class Getter(object): def __init__(self, base_url): self.base_url = base_url def get(self, resource): return requests.get(self.base_url + '/' + resource).text if __name__ == '__main__': getter = Getter('https://api.github.com') print(getter.get('timeline.json'))
こんな感じです。実用性ないしWindowsで動かねえぞとかGitHubにそんなAPIねえぞとか色々な突っ込みはあるでしょうが、あくまで例なので空気を読みましょう。
機能追加
getリクエストを送る、前と後に処理を挟み込みたいという気持ちになってください。なれましたか?
次にどうやって実装するかを考えましょう。
一番最初に思いつくのは、get_hogehoge, get_pre_hugahuga
みたいなメソッドを増やしてしまうことでしょうか。それとも呼び出し側で前後に素直にベタ書きしますか?
ところが霊感が降ってきた貴方は、以下のような実装にします。
# -*- coding: utf-8 -*- from __future__ import (print_function, division, absolute_import, unicode_literals, ) import requests class Getter(object): def __init__(self, base_url): self.base_url = base_url from collections import defaultdict self.listeners = defaultdict(list) def add_eventlistener(self, event_type, func): self.listeners[event_type].append(func) def get(self, resource): self._pre_get() ret = requests.get(self.base_url + '/' + resource).text print('info: get request end.') self._on_get() return ret def _on_get(self): for f in self.listeners['get']: f() def _pre_get(self): for f in self.listeners['preget']: f() if __name__ == '__main__': getter = Getter('https://api.github.com') getter.add_eventlistener('get', lambda: print('one')) getter.add_eventlistener('preget', lambda: print('pre one')) getter.add_eventlistener('get', lambda: print('two')) print(getter.get('timeline.json')) # pre one # info: get request end. # one # two # {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"} getter.add_eventlistener('get', lambda: print('threeeee')) print(getter.get('timeline.json')) # pre one # info: get request end. # one # two # threeeee # {"message":"Bad credentials","documentation_url":"https://developer.github.com/v3"}
はい。
これで「挟み込む処理を外部に出しつつ再利用性が高い処理」を実現出来ましたね。多分。
なおPythonに詳しくない方の為に解説すると、defaultdict
は存在しないキーでアクセスするとコンストラクタに渡した型で自動的に初期化されるという便利なクラスです。
JavaScriptで書くとdic[key] = dic[key] == undefined ? new some_type() : dic[key]
みたいな感じですかねきっと。
wikipediaとかで説明されている実装とはだいぶ趣きが違うと思いますが、エッセンスとしてはこんな感じです。
またaddObserver
がadd_eventlistener
に改名されていますが、そういうことです。JavaScriptのアレはまんまオブザーバーパターンです。
ジャバだといちいちObserverを継承したクラスを定義するか、匿名クラスを使っていることが多いリスナーですが、Java8や現代の言語は普通にラムダ式を使った方が便利なのでそういう感じになっています。
リスナに引数を渡したい、リスナを削除したい、色々な要求はあるでしょうがそういう場合はそういう感じに改造しましょう。
抽象化
Pythonはダックタイピングな言語なのでわざわざインターフェース化する必要はないのですが、せっかくなので今回の実装を抽象化した上でそれを継承する形にして今回のゲッタークラスを再実装してみましょう。
こうなります。
class Subjectable(object): @property def listeners(self): ''' :rtype: dict ''' return self._listeners def add_eventlistener(self, event_type, func): self.listeners[event_type].append(func) def notify(self, event_type): for f in self.listeners[event_type]: # イベントオブジェクトや自身を渡したければ f(e) みたいな定義にすれば良い f() class Getter(Subjectable): def __init__(self, base_url): self.base_url = base_url from collections import defaultdict self._listeners = defaultdict(list) def get(self, resource): self.notify('get') ret = requests.get(self.base_url + '/' + resource).text self.notify('preget') return ret
なんだか急にパターンっぽくなりましたね? これなら同僚とかにも再利用してもらえそうです。
ここまでくれば、メソッドの前後に自動でリスナの発火を挟み込むデコレータを書いたりと好きなことが出来ますね。
まとめ
だいぶ我流な実装なので、Gof的な意味でいうと間違った実装なのですが本質はハズしていない(はず)です。
いきなりクラス図と意味の分からない擬人化ストーリーで説明されるよりはわかりやすいはずだと信じているので、このエントリでなんとなくわかったつもりになってから結城さんのデザパタ本とかを読むといいんじゃないんでしょうかね。
個人的にはデザパタって今さら本を買ってまで頑張る必要があるようなもんかなーとは思いますが。
とまれ、JavaScriptだとES6でObject.observe()
が予定されていますし、Objective-CだとKVO
やNSNotificationCenter
など(Swiftだとどうなるんでしょうね)、GUIアプリを作る上では必須の知識というか、前提知識みたいなノリで話を振られたり記事が書かれたりするものなので頑張りましょう。
以上そんな感じです。
- 作者: 結城浩
- 出版社/メーカー: SBクリエイティブ株式会社
- 発売日: 2014/02/14
- メディア: Kindle版
- この商品を含むブログ (1件) を見る