no-image

手を動かして学ぶObserverパターン@Go

先日のIteratorパターンに続き今回はObserverパターンについて。

前回はTypeScriptを使って書いてみたが、今回はGoで実装してみる。

この記事では、以下のことについて書く。

  • Observerパターンの概要
  • Goでのオブジェクト指向プログラミングの概要
  • シンプルなObserverパターンモデルの作成

Observerパターンの概要

Observerパターンは、あるオブジェクトのイベントを他のオブジェクトへ通知するときに用いられるものだ。

以下はWikiにあったクラス図。

Observer-pattern-class-diagram.png

【画像引用元】Observer パターン – Wikipedia

Subject(観察される側)とObserver(観察する側)の2つのクラスに分かれる。

Subject

Observerに監視されているオブジェクト。
一つのSubjectを複数のObserverが監視する。

誰に監視されるかは追加や削除が可能で、100個のObserverに監視されたり、1個のObserverに監視されたりできる。

登録時、削除時にそれぞれaddObserver、deleteObserverメソッドを使う。

では、残りのnofityObserversは何かというと、これがまさに「監視しているObserverに通知を送る」メソッド。
これ一つを実行すれば、このSubjectを監視しているすべてのObserverに通知を送ることができる。

Observer

通知される側。Subjectを監視している。

notifyメソッドを持っており、これがSubject側で実行されることで通知が来る。

どこで使われるか

並列処理の一つで使われたりする。

I/O待ちの処理があり、この処理が完了したタイミングで他のなにかの関数を実行したいときなどに使われる。

同じような感じで、なにかイベントが起こったとき、例えば「キーボードが押された」ときに別の処理を実行したい場合も考えられる。

RSSも似たようなイメージだ。
100人の人(Observer)が、Aさんのブログ(Subject)を読みたいと思いRSSに登録してあると、ブログが公開されたときに100人全員に自動的に通知が行く。

Publish,Subscribeパターン

SubjectとObserver、これら2つの命名が少しややこしく、実際Observerは常に監視しているというよりは、能動的に通知を待っている感じなので、Publish/Subscriveパターンとも言うらしい。

Publishが出版側、Subscribeが購読する側だ。

【参考】

Goでのオブジェクト指向プログラミング

Go言語はクラスやオブジェクトなどの概念のない、手続き型の言語だ。
しかし、構造体を使うことでオブジェクト指向っぽいプログラミングができる。

以下の構造体Personをnameとageというプロパティを持ったクラスっぽく使うわけだ。

では、メソッドはどうするのかというと、レシーバというものを使って定義する。

通常の関数定義が以下のようなものに対し、

レシーバを使ったメソッド定義は以下のようになる。

funcと関数名の間にレシーバの名前と型を記述することで、その構造体に対するメソッドを定義できる。

そして、以下のように初期化してメソッドを使う。

以上のようにして、クラスのないGoでもオブジェクト指向っぽいプログラミングができる。

structとinterfaceの違い

上で見た構造体を宣言するときに使った「type」句はインターフェースに名前を付けるときにも使われる。

構造体にはプロパティとその型を列挙するのに対し、インターフェースにはメソッドとその型のみを列挙する。

Goにはimplementsなどもなく、ある構造体Aに、あるインターフェースBに列挙されたメソッドを全て持たせれば、その構造体AはインターフェースBを実装したことになる。

つまりは、ダックタイピングができる。

【参考】

あと、余談だがGoでは短い変数名を使うことが多い。
それについては以下の記事を参考。

【参考】 sigbus.info: Goの変数名が短い理由(あるいはGoがほかの言語と違う理由)

シンプルなObserverモデルを実装する

前置きが長くなったが、これからObserverパターンを書いていく。
まず最初はとてもシンプルなものを作ってみる。
継承などもしない具象クラスのみのものだ。

軽くモデルを説明しておく。
出版社とそこから出る本を購入したい読者2名だ。

読者は本が発売されたその日に本を手に入れたいが、発売日も知らないし問い合わせても教えてくれない。
「発売されたら連絡もらえますか」と聞くも「一人ひとりに連絡すると大変なので」と断られる。

その代案として提案されたのが「出版社を監視してください」というもの。

これなら購入したい人数が増えても出版社側の手間は変わらず、出版されたタイミングで監視している人みんなが知ることができる。

(ん?これ現実問題解決してるのか?まぁいいか…)

という感じなものを作っていく。
Subjectが出版社で、Observerが読者だ。

完成形は以下のページで試せる。
Go Playground

GitHubはここ

Observer

先にObserverから書いていく。

余談だが、Goでは「self」や「this」は使わないようだ。
【参考】Goのメソッドレシーバの命名慣習 – Qiita

constructor

コンストラクタという機能はGoにはないが、以下のパターンで定義できる。
上で見た構造体の初期化処理のラッパーだ。

Goでは慣習的にコンストラクタ名にはprefixとして「new」を付け、「new + 構造体名」が用いられている。

ちなみに、new()関数を使う方法もある。書き換えてみると

他の言語ではこっちに近い。

interfaceの実装

Observerパターンに必須のnotify()メソッドを定義する。
これでObserverインターフェースは実装されたことになる。

Subject

次にSubjectを作る。
監視される側。例では出版社だ。

今回は、監視されるObserverの全てのnotifyを実行するnotifyObserver()メソッドと、監視下に置かれるためのaddObserver()を実装した。

struct

まずはこの構造体の定義について見る。

この[]Observerというのは何だ?

これは下の方で定義しているObserverインターフェースだ。
このデザインパターンではObserver側ではnotify()メソッドを持つことが必須である。

それを強制するためにObserversの型にnotifyメソッドを持ったインターフェースの型を当てている。

method

レシーバを使うことでメソッドを定義できるのは上で見たが、ここにポインタレシーバを使うものと、値レシーバを使うものとがある。

これには使い分けの基準があるが、今回の場合は「値を書き換えるかどうか」で分けた。
ちゃんとした基準などは参考リンクを参照。

値を書き換える場合はポインタレシーバを使う。

値を読むだけの場合は以下のように値レシーバでいい。

そもそも動くかどうかはもちろんのこと、パフォーマンス的な話などを加味する必要があり、mapとsliceの違い、スタックとヒープに置かれるものの差などGoの言語仕様を理解して使い分ける必要がある。

【参考】

さらに、Goコンパイラがよしなに行ってくれる暗黙の型変換も知っておいたほうが良さそう

【参考】Go の関数レシーバが暗黙の型変換される場合とされない場合のまとめ – Qiita

main

それでは、上で作ったものを実行してみる。

結果。

Observerを登録後、一発notifyObserversを実行すれば、全員に通知が飛ぶわけだ。

所感

前回はTypeScriptを使ってIteratorパターンを実装してみた。
今回はGoでObserverパターンを実装してみた。
適当に始めたが、この「様々な言語で様々なデザインパターンを組んでみる」というのはなかなか学びが深いなと感じた。

デザインパターンの概念はもちろんのこと、言語仕様、記法などを手を動かしながら奮闘できる。

なかなか良いので、今後も今知りたいデザパタを見つけたら何かで組んでみよう。

参考

疑問

話はここで終わりだが、この記事を書いた段階で、interfaceについてのどこがわかっていてどこがわかっていないのかを、未来の自分のためにメモしておく。

Goで書き始めて思ったこと

sturctはプロパティを列挙でき、interfaceはメソッドの定義をする。
逆に以下のようにstuctの中に、メソッドはかけない。

このように構造体をクラスっぽく扱うとき、「Observerクラスにnotify()メソッドを持つことを強制したい」のだが、それを実現するためには他の言語ではプロパティとメソッドを持ったインターフェースをimplementsさせたりする。

だが、Goにはそういうように強制させる書き方がないので、難しく感じる。実装を矯正させてコンパイルエラーを吐かせたいのだ。

以下が簡潔に答えられればたぶんスッキリする。

  • interfaceがあることで誰が得をするのか(構造体の作者?利用者?また別の人?)
  • interfaceがなくても目的のものは作れるのか、もしくはinterfaceがあることであるメソッドの作成を強制できるのか
  • stuct内にinterfaceを埋め込んだときの具体的な利用状況

疑問の参考

以下、現時点では幾何かの理解不足のため、一部内容を理解できなかった点があった記事。

最後の記事に関しては、例えば以下のコード。NewDuckの定義のときの返り値の型はDuck(インターフェース)だが、returnしているのはduck(構造体のアドレス)。とか。(動く)

また、機会を見つけて調べてまとめる。