【Swift】Observerパターン入門

はじめに

デザインパターンの1つ、Observer パターンについて書きたいと思います。

と言うのも、この後に RxSwift の勉強をしたいがため、改めてまずはここからでしょうと思ったゆえです。

まだObserverパターンを知らない方がいましたら、一緒に Observer → RxSwift と勉強していけたら幸いです。

サンプルコードの動作確認の環境は以下の通りです。

  • macOS: Mojave (10.14.6)
  • Xcode: 11.3
  • Swift: 5

Observerパターンってどんなパターン?

そもそも、デザインパターンって何?

デザインパターンと言うのは、簡単に言えば「過去の偉人たちが編み出したプログラムの定石的な組み方を、みんなでガンガン使っていけるようにまとめたもの」です。

例えば、「アプリ全体で使う共通の設定などをまとめたクラスがあるんだけど、これはどこからでも複数インスタンスを作成されるとまずいんだよな」と思ったなら、「あるよ。Singletonパターン」となります。

例えば、「コマンドを待ち受けるクラスがあるんだけど、これを“待機中”や“コマンド処理中”なんかの状態によって異なる動きをするようにしたいな。ああだけど、呼び出す側はそうした状態を意識しないで使えるようにしたい」と思ったなら、「あるよ。Stateパターン」となります。

このように、ただ動くだけのプログラムから脱却し、一歩進んだコーディングを志した時に見るべきテクニックの集合体がデザインパターンとなっています。

また、iOS SDKに限らず、世にあるSDKやライブラリの多くのコンポーネントがこのデザインパターンに則って書かれているので、できる限り学んでおいて損はありません。

もっとも、それらを使う際にその知識が必要となる場合も多々あるので、結局のところはコードを書く人にとっては避けては通れない道ですね。

より詳しく知りたい方は、Let’s google!

Observerパターンとは?

Observer(監視者)の言葉からイメージできるかと思いますが、そのとおり「あいつに動きがあったらすぐに伝えろ。こちらもすぐ動く」なパターンです。

こう書くとなんだか物騒ですが、要するに「あるオブジェクトの状態変化やイベント発生などの動きを、知りたい人がいちいち確認するのではなく、そのオブジェクトに自ら発信して知らせてもらおう」というものです。

アプリ開発においては

  • データに変更が発生したら、画面表示を更新する
  • ユーザーが検索条件欄に何かを入力したら、候補リストの内容を絞り込む
  • 非同期による通信処理が終わったら、その完了と結果をユーザーにダイアログ表示で伝える

など、「何かの動きに合わせて、別な何かを動かしたい」ような要件に使われるのが、このObserverパターンです。

この例で言うと、監視対象となる「データ」、「検索条件欄」、「通信処理」を Subject と呼び、それらを監視する人を Observer(または、Listener) と呼びます。

UIButtonのタップイベントや、URLSessionのdataTaskメソッドで渡すcompletionHandlerなど、よく見る場所に多くこのObserverパターンが使われています。

Publish/Subscribeモデル

Observerパターンを↑のように呼ぶこともあります。

Publish(出版)とSubscribe(購読)の名が示すとおり、「出版社が新たな出版物を出版したら、それが購読者に速やかに届けられる」形のプログラム設計を言います。

例えばYoutubeでは、ユーザーは好きなYoutuberのチャンネルに登録(Subscribe)することができ、何か新しい動画が投稿(Publish)されたら、通知を受けてすぐ観に行くことができます。

これがまさしくPublish/Subscribeモデルです。

Observerパターンも、ObserverがSubjectの動きを確認すると言うよりは、Subjectが自らOberverたちに通知する形であるため、個人的には「Publish/Subscribeモデル」の呼び方のほうがしっくり来たりします。

ちなみに、Publish/Subscribeモデルは省略して「Pub/Sub」(パブサブと発音)と呼ばれたりもします。

GoogleのクラウドインフラサービスであるGCPに、このモデルを使った「Cloud Pub/Sub」なるメッセージ配信サービスがあるため、GCPを使った案件で単に「Pub/Sub」と言われると、Pub/SubモデルのことなのかPub/Subサービスのことなのかわからなくなることがあります。

で、どう書くの?

具体的なコードは下記にて示しますが、要点だけ言うと

  • Subjectクラスを用意して、以下を実装する
    • Observerを保持するための変数を持つ
    • Observerの追加や削除を行うメソッドを持つ
    • 何か動きがあった時に、それを保持するすべてのObserverに知らせるメソッドを持つ
  • Observerクラスを用意して、以下を実装する
    • Subjectに変更を通知してもらうためのPublicメソッドを持つ
    • 変更の通知を受けた時の何かしらの処理を書く

となります。

これを図で表すとこうなります 。(他力本願)

サンプルコード

実装

以下にサンプルコードを記載します。
コードはXcodeのPlaygroundで実行できます。

上でYoutubeの例えが出ましたので、それをイメージした形となっています。
また、コード中ではObesrverをListenerとして記述していますのでご注意を。

では↓

import Foundation

/// Observer 基底クラス
class Listener: NSObject {
}

/// Subject 基底クラス
class Subject<E: Listener> {
    
    // リスナーを保持する配列
    private(set) var listeners = Array<E>()
    
    /// リスナー追加
    func addListener(_ listener: E) {
        listeners.append(listener)
    }
    
    /// リスナー削除
    func removeListener(_ listener: E) {
        if let index = listeners.firstIndex(of: listener) {
            listeners.remove(at: index)
        }
    }
}

/// Subject 実装クラス
class Youtuber: Subject<Subscriber> {
    
    init(_ name: String) {
        self.name = name
    }
    
    private(set) var name: String

    /// 動画を投稿
    func post(_ video: Video) {
        // Youtubeに動画を投稿
        let url = YoutubeUtil.post(youtuber: self, video: video)
        
        // チャンネル登録者に通知する
        notifyUpdate(video, url: url)
    }
    
    /// 変更通知
    func notifyUpdate(_ video: Video, url: String) {
        print("\(name): \(url)に動画投稿したよ!")
        for listener in listeners {
            listener.didUpdate(youtuber: self, video: video, url: url)
        }
    }
}

/// Observer 実装クラス
class Subscriber: Listener {
    
    init(_ name: String) {
        self.name = name
    }
    
    private(set) var name: String

    func didUpdate(youtuber: Youtuber, video: Video, url: String) {
        print("  -> \(name): \(youtuber.name)さんの「\(video.title)」って動画見るぜ!")
        
        // Youtubeアプリ起動
        YoutubeUtil.launchApp(url)
    }
}

/// Video クラス
class Video {
    
    init(_ title: String) {
        self.title = title
    }
    
    private(set) var title: String
}

/// Youtube ユーティリティクラス
class YoutubeUtil {

    static func post(youtuber: Youtuber, video: Video) -> String {
        return "https://www.tubeyou.com/\(youtuber.name)/\(video.title)"
    }

    static func launchApp(_ url: String) {
        // Youtubeアプリを起動してURLの動画を開く処理とか
    }
}

// ユーチューバー
let hikakin = Youtuber("ヒカキン")
let hajime = Youtuber("はじめしゃちょー")

// チャンネル登録者
let tanaka = Subscriber("田中")
let suzuki = Subscriber("鈴木")
let yamada = Subscriber("山田")

// チャンネル登録
hikakin.addListener(tanaka)
hikakin.addListener(suzuki)
hikakin.addListener(yamada)
hajime.addListener(tanaka)
hajime.addListener(suzuki)
hajime.addListener(yamada)

// 動画投稿
hikakin.post(Video("hikakin_douga_1"))
hajime.post(Video("hajime_douga_1"))

// チャンネル登録解除
hikakin.removeListener(tanaka)
hajime.removeListener(yamada)

// 動画投稿
hikakin.post(Video("hikakin_douga_2"))
hajime.post(Video("hajime_douga_2"))

クラス説明

上から順に説明します。

Listener クラス 】

Observerの基底クラスです。
よくあるサンプルではこのクラスの中に通知を受け取った時のメソッドを定義していたりしますが、実際には通知時にあれこれ渡すものが変わったりするので、それはサブクラスにまかせてここは中身空っぽとしています。

Subject クラス 】

Subjectの基底クラスです。
これもよくあるサンプルでは、このクラスの中ですべてのObserverの通知用メソッドを呼び出す処理を書いたりしていますが、要件により通知時に渡すものを自由に決めたいため、すべてに共通するObserverを管理する配列とメソッドのみの実装にとどめています。

Youtuber クラス 】

Subjectの実装クラスです。
動画を投稿(postメソッドを使用)したら、登録ユーザーに通知(notifyUpdate)しています。

もし名前が変わった場合も通知したい場合は、nameプロパティにdidSetを付けて、その中で別途それ用のListenerの通知メソッドを呼ぶなどの実装方法が考えられます。

Subscriber クラス 】

Observerの実装クラスです。
Youtuberのチャンネル登録が可能なようにListenerを継承し、通知を受けた(didUpdateが呼ばれた)らYoutubeアプリを起動して視聴するようなことをしています。

上記Youtuberクラスの説明にもあるように、Youtuberの名前が変わった場合にも通知を受けたい要件が出た場合には、didUpdateNameなどの新たなメソッドを用意するか、あるいはdidUpdateを動画投稿とどちらでも使える汎用的な形にするか、などの方法が考えられます。

Video クラス 】

大した意味はありません。サンプルのためのデータクラスです。

YoutubeUtil クラス 】

こちらも大した意味はありません。それっぽい感じの適当クラスです。
postして返されるURLも、飛べたりしないようにでたらめです。

実行

実行すると以下のような結果が表示されると思います。

ヒカキン: https://www.tubeyou.com/ヒカキン/hikakin_douga_1に動画投稿したよ!
   -> 田中: ヒカキンさんの「hikakin_douga_1」って動画見るぜ!
   -> 鈴木: ヒカキンさんの「hikakin_douga_1」って動画見るぜ!
   -> 山田: ヒカキンさんの「hikakin_douga_1」って動画見るぜ!
 はじめしゃちょー: https://www.tubeyou.com/はじめしゃちょー/hajime_douga_1に動画投稿したよ!
   -> 田中: はじめしゃちょーさんの「hajime_douga_1」って動画見るぜ!
   -> 鈴木: はじめしゃちょーさんの「hajime_douga_1」って動画見るぜ!
   -> 山田: はじめしゃちょーさんの「hajime_douga_1」って動画見るぜ!
 ヒカキン: https://www.tubeyou.com/ヒカキン/hikakin_douga_2に動画投稿したよ!
   -> 鈴木: ヒカキンさんの「hikakin_douga_2」って動画見るぜ!
   -> 山田: ヒカキンさんの「hikakin_douga_2」って動画見るぜ!
 はじめしゃちょー: https://www.tubeyou.com/はじめしゃちょー/hajime_douga_2に動画投稿したよ!
   -> 田中: はじめしゃちょーさんの「hajime_douga_2」って動画見るぜ!
   -> 鈴木: はじめしゃちょーさんの「hajime_douga_2」って動画見るぜ!

見たまんまですが、YoutuberとSubscriberのインスタンスをそれぞれ作成し、チャンネル登録(addListener)していれば動画投稿(post)された時に通知される、解除(removeListener)すれば通知されなくなる、といった動きが確認できるかと思います。

おわりに

正直Observerパターンは、知ってから書くと言うより、今まで書いていたものが実はObserverパターンと呼ばれるものだった、というケースのほうが多いように思います。

とは言え、それをきちんとObserverパターンと認識した上で書くのとそうでないのとでは、やはりどこか差が出るものではないかと思いますので、そういった意味でもObserverパターンやその他のパターンを学んでおくことには意義があるのではないかと思います。

自分もまだまだすべてのパターンを学べているわけではないので、これからも精進していきたいなと思う今日このごろです。

それでは。