カナデKanade

4. オブザーバーパターン (Observer)

状態が変わる時に購読者たちに自動で知らせるオブザーバーパターンについて学びましょう。

ひとことで言うと
あるオブジェクトに何か起きた時、それを知りたい側が自分で購読を申し込んでおき、オブジェクトは購読者リストにいる全員に知らせるだけにするパターンです。

問題: だれに連絡するかを全部知っているオブジェクト

ネットショップで、品切れだったスニーカーが再入荷したとしましょう。この時やることはいくつもあります。

  • 在庫画面の数量を更新します。
  • 再入荷のお知らせを申し込んだお客さんにSMSを送ります。

まず思いつくのは、商品が入荷した時に、知らせるべき相手に1つずつ直接連絡する方法です。

【在庫画面】を設計しよう:
    〈更新する〉を作ろう(【文字列】の『商品』, 【数字】の『数量』):
        枠「[画面] {『商品』} 残り: {『数量』}」を出力しよう

【SMS送信】を設計しよう:
    〈送る〉を作ろう(【文字列】の『宛先』, 【文字列】の『内容』):
        枠「[SMS → {『宛先』}] {『内容』}」を出力しよう

(参考:商品が入ってきたら、知らせるべき相手に1つずつ直接連絡します)
【商品】を設計しよう:
    『名前』を【文字列】の「」にしよう
    『数量』を【数字】の0にしよう
    『画面』を【在庫画面】の新しい【在庫画面】()にしよう
    『SMS』を【SMS送信】の新しい【SMS送信】()にしよう

    最初に作られる時(【文字列】の『名前』)次のようにしよう:
        『私』の『名前』を『名前』にしよう

    〈入荷する〉を作ろう(【数字】の『個数』):
        『私』の『数量』を(『私』の『数量』 + 『個数』)にしよう
        『私』の『画面』の〈更新する〉(『私』の『名前』, 『私』の『数量』)を実行しよう
        『私』の『SMS』の〈送る〉(「美咲」, 枠「{『私』の『名前』} 再入荷のお知らせ!」)を実行しよう
        『私』の『SMS』の〈送る〉(「翔太」, 枠「{『私』の『名前』} 再入荷のお知らせ!」)を実行しよう

『スニーカー』を【商品】の新しい【商品】(「スニーカー」)にしよう
『スニーカー』の〈入荷する〉(5)を実行しよう

実行結果
[画面] スニーカー 残り: 5
[SMS → 美咲] スニーカー 再入荷のお知らせ!
[SMS → 翔太] スニーカー 再入荷のお知らせ!

ちゃんと動きます。でも、お店が大きくなると痛くなります。

  • 商品が連絡先を全部知っていなければなりません。【商品】は「数量が増えた」ことだけを知っていればいいオブジェクトなのに、【在庫画面】と【SMS送信】を直接持ち、お客さんの名前まで覚えています。
  • 新しいお客さんがお知らせを申し込むには、コードを直す必要があります。 陽菜さんが申し込んだら、〈入荷する〉に行を1つ足さなければなりません。プログラムが動いている間に申し込みを受け付ける方法がありません。
  • お知らせをオフにすることもできません。 翔太さんが「もうお知らせはいりません」と言っても、コードからその行を消すしかありません。
  • 新しく知らせる相手ができるたびに商品が大きくなります。「管理者に記録を残す」「おすすめリストを更新する」ができるたびに、【商品】に新しいオブジェクトと新しい行がくっつきます。

解決策: 購読を受け付けて、知らせるだけにする

オブザーバーパターンは、連絡の向きを変えます。商品がだれに連絡するかを決めるのではなく、知りたい側が申し込みます。

  1. 知らせを受け取りたい側が守る約束、つまり規定を作ります。購読者なら〈知らせを受ける〉ができなければなりません。
  2. 商品は購読者のリストだけを持ちます。〈購読する〉でリストに入れ、〈購読をやめる〉でリストから外します。
  3. 何か起きたら、商品はリストにいる全員の〈知らせを受ける〉を呼ぶだけです。相手が画面なのかお客さんなのかは知らなくてかまいません。
  4. 知らせを受け取った側は、それぞれ自分でやることをします。画面は更新し、お客さんはSMSを受け取ります。

たとえ: YouTubeのチャンネル登録と通知

YouTuberは新しい動画を投稿するだけです。だれが通知を受け取るかは視聴者が決めます。登録ボタンを押せばリストに入り、通知をオフにすればリストから外れます。YouTuberは、登録者が何人いても、だれが入ってだれが抜けても、動画の投稿のしかたを変える必要はありません。視聴者の1人1人が「オブザーバー(見守る人)」です。

だれが何をするの?

役割 この例では やること
購読者の約束 【購読者】(規定) 知らせを受け取る側ができなければならないことを決めます
知らせを出す側 【商品】 購読者リストを管理し、何か起きたら全員に知らせます
具体的な購読者 【在庫画面】、【お客さん】 知らせを受け取ったら、それぞれ自分のやることをします

カナデで作る

同じお店をオブザーバーパターンで作り直してみましょう。今回は、途中で翔太さんがお知らせをオフにする場面も入れました。

(参考:ステップ1 — 知らせを受け取りたい側が守る約束)
【購読者】を規定しよう:
    〈知らせを受ける〉がなければならない(【文字列】の『商品』, 【数字】の『数量』)

(参考:ステップ2 — 商品は購読者のリストだけを持っていて、何かあったら全員に知らせます)
【商品】を設計しよう:
    『名前』を【文字列】の「」にしよう
    『数量』を【数字】の0にしよう
    『購読者たち』を【】に隠そう

    最初に作られる時(【文字列】の『名前』)次のようにしよう:
        『私』の『名前』を『名前』にしよう

    〈購読する〉を作ろう(【購読者】の『だれ』):
        『私』の『購読者たち』に『だれ』を追加しよう

    〈購読をやめる〉を作ろう(【購読者】の『だれ』):
        『残る人』を【】にしよう
        『私』の『購読者たち』の『購読者』ごとに繰り返そう:
            もし(『購読者』が『だれ』と違う)なら:
                『残る人』に『購読者』を追加しよう
        『私』の『購読者たち』を『残る人』にしよう

    〈入荷する〉を作ろう(【数字】の『個数』):
        『私』の『数量』を(『私』の『数量』 + 『個数』)にしよう
        『私』の『購読者たち』の『購読者』ごとに繰り返そう:
            『購読者』の〈知らせを受ける〉(『私』の『名前』, 『私』の『数量』)を実行しよう

(参考:ステップ3 — 知らせを受け取った側は、それぞれ自分のやることをします)
【購読者】に従う【在庫画面】を設計しよう:
    〈知らせを受ける〉を作ろう(【文字列】の『商品』, 【数字】の『数量』):
        枠「[画面] {『商品』} 残り: {『数量』}」を出力しよう

【購読者】に従う【お客さん】を設計しよう:
    『名前』を【文字列】の「」にしよう

    最初に作られる時(【文字列】の『名前』)次のようにしよう:
        『私』の『名前』を『名前』にしよう

    〈知らせを受ける〉を作ろう(【文字列】の『商品』, 【数字】の『数量』):
        枠「[SMS → {『私』の『名前』}] {『商品』} 再入荷のお知らせ!」を出力しよう

『スニーカー』を【商品】の新しい【商品】(「スニーカー」)にしよう
『美咲』を【購読者】の新しい【お客さん】(「美咲」)にしよう
『翔太』を【購読者】の新しい【お客さん】(「翔太」)にしよう

『スニーカー』の〈購読する〉(新しい【在庫画面】())を実行しよう
『スニーカー』の〈購読する〉(『美咲』)を実行しよう
『スニーカー』の〈購読する〉(『翔太』)を実行しよう
『スニーカー』の〈入荷する〉(5)を実行しよう

「(翔太さんが通知をオフにしました)」を出力しよう
『スニーカー』の〈購読をやめる〉(『翔太』)を実行しよう
『スニーカー』の〈入荷する〉(3)を実行しよう

実行結果
[画面] スニーカー 残り: 5
[SMS → 美咲] スニーカー 再入荷のお知らせ!
[SMS → 翔太] スニーカー 再入荷のお知らせ!
(翔太さんが通知をオフにしました)
[画面] スニーカー 残り: 8
[SMS → 美咲] スニーカー 再入荷のお知らせ!

最初の入荷はリファクタリング前と結果が同じです。2回目の入荷では翔太さんが抜けましたが、【商品】のコードは1行も変わっていません。

ステップごとにもう一度

  • ステップ1 — 約束を作る。【購読者】は「知らせを受け取れれば購読者」という約束です。商品はこの約束だけを信じて〈知らせを受ける〉を呼びます。
  • ステップ2 — リストだけを管理する。【商品】には【在庫画面】も、お客さんの名前もありません。『購読者たち』というリストが1つあるだけです。このリストは隠そうで作ったので、外からは〈購読する〉と〈購読をやめる〉でしか変えられません。
  • 購読をやめるのは「抜いて作り直す」。〈購読をやめる〉は、リストを回りながら、やめる人ではない購読者だけを新しいリストに入れます。(『購読者』が『だれ』と違う)は、2つのオブジェクトが同じオブジェクトかどうかを比べます。同じ名前の別のお客さんがいても混ざりません。
  • ステップ3 — それぞれ自分で。 知らせを受け取った【在庫画面】と【お客さん】は、やることがまったく違います。それでも商品は気にしません。

知らせる順番はどう決まるの?
購読した順番です。『購読者たち』はリストなので、〈購読する〉で入れた順に〈知らせを受ける〉が呼ばれます。順番が大事な仕事(例: 画面を先に更新してからSMSを送る)があるなら、購読する順番をそろえるか、そもそも順番に頼らないように作るのがおすすめです。

新しい購読者を足してみると

「管理者の記録帳」も入荷の知らせを受け取りたいとしましょう。

  • 前のやり方:【商品】に【記録帳】オブジェクトを入れて、〈入荷する〉に行を1つ足さなければなりません。
  • オブザーバーパターン:【購読者】に従う【記録帳】を作って、『スニーカー』の〈購読する〉(新しい【記録帳】())で申し込むだけです。【商品】はそのままです。

やってみよう
上のコードに【記録帳】を足してみてください。知らせを受け取ったら[記録] スニーカー入荷、現在8個のように出力します。最初の入荷の前に購読させて、結果に記録が2回出るか確かめてみましょう。次に陽菜さんというお客さんも足して、2回目の入荷の前に購読させてみてください。

どんな時に使うといいの?

  • 1つのオブジェクトに何か起きた時、知るべき相手がいくつもあり、その数が変わりうる時
  • だれが知らせを受け取るかが実行中に決まる時(購読の申し込み、通知オフ)
  • 知らせを出す側が、受け取る側の具体的な種類を知らなくて済むようにしたい時
  • 画面、通知、記録のようにお互い関係のない仕事が、同じ出来事に反応しなければならない時

いいところと気になるところ

いいところ 気になるところ
新しい購読者を足しても、知らせを出す側を直しません だれが知らせを受け取るのか、コードを見ただけではひと目で分かりません
実行中に購読したりやめたりできます 知らせる順番に頼ると混乱しやすいです
知らせを出す側と受け取る側が、お互いを詳しく知らなくて済みます 購読者が多いと、1回知らせるたびにやることが増えます
購読者がさらに知らせを出すと連鎖的につながって、追いかけにくくなることがあります

早すぎるパターンは毒です。
知らせる相手がちょうど1つで変わる予定もないなら、そのオブジェクトを直接呼ぶほうが読みやすいです。知らせる相手が増えてきたり、実行中に申し込みや解除を受け付ける必要が出てきたりしてからオブザーバーに変えても、遅くはありません。

ほかのパターンとの関係

  • ストラテジーパターンと形が似ています。 どちらも「規定に従うオブジェクトに仕事を任せる」ものです。ストラテジーは1つを選んで差し込んで任せ、オブザーバーはリストにいる全員に知らせます。
  • コマンドパターンと一緒に使えます。 購読者の代わりに「やること」のコマンドオブジェクトをリストに入れておき、何か起きたら全部実行させることもできます。
  • シングルトンパターンとよく出会います。 プログラム全体の知らせ(ログイン、設定の変更)を出すオブジェクトは1つだけであるべきなので、シングルトンにしていろいろな場所から購読することがよくあります。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。