冬のセール!

Observer

別名:Event-Subscriber、Listener、オブザーバー、イベント・サブスクライバー、リスナー

一言でいうと

Observer オブザーバー 観察者 振る舞いに関するデザインパターンの一つで サブスクリプション 通知申し込み の仕組みを定義することにより 観察対象オブジェクトに何かイベントが発生した時 そのイベントの観察者である複数のオブジェクトへ通知を行います

Observer デザインパターン

問題

Customer 顧客 Store 店舗 という 2 種類のオブジェクトがあるとします 顧客は特定の製品ブランド たとえば iPhone の最新モデル に非常に興味を持っていて それは近日中に店舗で発売開始の予定です

客は毎日店舗を訪れ 商品があるかどうかを確認することも可能です しかし 製品はまだ配送中なので ほとんどの訪問は無駄に終わることになるでしょう

店舗への訪問 対 スパム送信

店舗への訪問 対 スパム送信

一方 店は 新しい製品が入荷するたびに すべての顧客に スパムとみなされる可能性がある 大量の電子メールを送信することも可能です これで 顧客の何割かは 度重なる店舗への訪問をせずにすみ 助かります 同時に 新製品には関心のない顧客を怒らせることになります

対立した状況です 顧客が製品の入荷状況を調べるために時間を無駄にするか それとも店が不適切な顧客に通知をして資源を無駄にするか

解決策

関心を惹かれるような状態を持つオブジェクトは しばしば subject 主体 とも呼ばれますが その状態の変化を他のオブジェクトに通知するため 我々は publisher 発行者 と呼ぶことにします パブリッシャーの状態の変化を追いかけたい他のオブジェクトは すべて subscriber 予約購読者 と呼ばれます

Observer パターンでは パブリッシャー・クラスにサブスクリプションの仕組みを追加し 個々のオブジェクトがそのパブリッシャーから来る一連のイベントの通知を依頼するか 通知を解除することができるようにします えっ 何だか難しそう 大丈夫です 複雑そうに聞こえますが 実はそれほどではありません 実際には この仕組みは サブスクライバー・オブジェクトへの参照のリストを格納するための配列フィールドと そのリストへのサブスクライバーの追加と削除をするためのいくつかの公開メソッドで構成されています

サブスクリプションの仕組み

通知申し込みの仕組みにより 個々のオブジェクトはイベントの通知を申し込める

こうしておけば 重要なイベントがパブリッシャーに発生するたびに そのイベントはサブスクライバーに渡され そのオブジェクトの特定の通知メソッドを呼び出します

実際のアプリには 同じパブリッシャー・クラスのイベントを追跡したい数十のサブスクライバー・クラスがあるかもしれません パブリッシャーがそれらのクラスに密に結合されるのは避けなければなりません また パブリッシャークラスが第三者によって使用されることになっている場合は 事前にそのようなクラスについては知りようがありません

そのため すべてのサブスクライバーが同じインターフェースを実装し パブリッシャーはサブスクライバーとそのインターフェースを介してのみ通信することが肝心です このインターフェースは パブリッシャーが通知と共に状況に関するデータを渡すために使用できる一連のパラメーターとともに 通知メソッドを宣言する必要があります

通知メソッド

パブリッシャーは サブスクライバー・オブジェクトの特定の通知メソッドを呼び出して通知

アプリに複数の異なる種類のパブリッシャーがあり サブスクライバーにすべてのパブリッシャーと互換性を持たせたい場合は さらに一歩進んで すべてのパブリッシャーが同じインターフェースに従うようにさせます このインターフェースには 少数のサブスクリプション用メソッドがあるだけで大丈夫です このインターフェースにより サブスクライバーは具体的なクラスと結合することなく複数のパブリッシャーの状態を観察できます

現実世界でのたとえ

雑誌と新聞の購読

雑誌と新聞の購読

新聞や雑誌を購読したら 次の号があるかどうかを確認するために店に行く必要はありません 代わりに 出版社は出版直後に 時には事前に 郵便受けに直接新しい号を配達します

出版社は購読者のリストを維持しており どの雑誌に興味があるのかを知っています 購読者は 出版社に新しい雑誌を送るのを停止してもらいたい時にはいつでもリストから抜けることができます

構造

Observer デザインパターンの構造Observer デザインパターンの構造
  1. パブリッシャー Publisher 他のオブジェクトが関心を持つイベントを発行します パブリッシャーがその状態を変えた時や何らかの行為を行なった時に このようなイベントが発生します パブリッシャーには サブスクリプションの仕組みがあり 新規サブスクライバーの参加や現サブスクラーバーの参加を取り消すことができます

  2. 新しいイベントが発生すると パブリッシャーはサブスクリプション・リストにアクセスし 各サブスクライバー・オブジェクトのサブスクライバー・インターフェースで宣言された通知メソッドを呼び出します

  3. サブスクライバー Subscriber インターフェースは通知用インターフェースを宣言します ほとんどの場合 update メソッド一つだけがあります このメソッドには 更新時にパブリッシャーがイベントの詳細情報を渡せるようにいくつかパラメータを持たせることもできます

  4. 具象サブスクライバー Concrete Subscribers パブリッシャーが発行した通知に応じて何らかのことを行います これらのクラスは パブリッシャーが具体的なクラスに結合されずにすむように すべて同じインターフェースを実装しなければなりません

  5. 通常 サブスクライバーは 更新を正しく処理するために周辺情報を必要とします このため パブリッシャーは 通知メソッドの引数として周辺データを渡すことがよくあります パブリッシャーは引数として自分自身を渡すことができ サブスクライバーは必要なデータを直接取得することができます

  6. Client クライアント パブリッシャーとサブスクライバーのオブジェクトを別々に作成し パブリッシャーの更新に対してサブスクライバーを登録します

擬似コード

この例では Observer パターンを適応して テキスト・エディター・オブジェクトがその状態の変化について他のサービス・オブジェクトに通知します

Observer パターン例の構造

オブジェクトに起きるイベントを他オブジェクトに通知

サブスクライバーのリストは常に変化します アプリの望ましい動作に従い オブジェクトは 通知の開始と停止が動的に可能です

この実装では エディター・クラスは サブスクリプション・リストを自身で管理しません その目的のために作られた特別なヘルパー・オブジェクトにその仕事を委任します そのオブジェクトを改善して 中央イベント・ディスパッチャーとして機能するようにもできます そうすると 任意のオブジェクトがパブリッシャーとして機能できます

プログラムに新しいサブスクライバーを追加する時 パブリッシャーがサブスクラーバーと同一インターフェースを通してやりとりする限り 既存のパブリッシャー・クラスの変更は必要ありません

// 基底パブリッシャー・クラスには、サブスクリプション管理用のコードと通知
// メソッドが含まれている。
class EventManager is
    private field listeners: hash map of event types and listeners

    method subscribe(eventType, listener) is
        listeners.add(eventType, listener)

    method unsubscribe(eventType, listener) is
        listeners.remove(eventType, listener)

    method notify(eventType, data) is
        foreach (listener in listeners.of(eventType)) do
            listener.update(data)

// 具象パブリッシャーは、サブスクライバーの一部にとって興味深い本当のビジ
// ネス・ロジックを含む。このクラスは、基底クラスから派生することも可能だ
// が、現実の世界ではそれは常に可能ではない。具象パブリッシャーがすでにサ
// ブクラスである場合があるからだ。その場合は、ここで行っているように、合
// 成を使ってサブスクリプションのロジックにパッチをあてられる。
class Editor is
    public field events: EventManager
    private field file: File

    constructor Editor() is
        events = new EventManager()

    // ビジネス・ロジックのメソッドは、変更に関してサブスクラーバーに通知
    // 可能。
    method openFile(path) is
        this.file = new File(path)
        events.notify("open", file.name)

    method saveFile() is
        file.write()
        events.notify("save", file.name)

    // ……


// これがサブスクライバーのインターフェース。お使いのプログラミング言語が
// 関数型をサポートしているのなら、サブスクラーバー階層を全部、関数の組で
// 置き換え可能。
interface EventListener is
    method update(filename)

// 具象サブスクライバーは、帰属するパブリッシャーから発行された更新通知に
// 反応。
class LoggingListener implements EventListener is
    private field log: File
    private field message: string

    constructor LoggingListener(log_filename, message) is
        this.log = new File(log_filename)
        this.message = message

    method update(filename) is
        log.write(replace('%s',filename,message))

class EmailAlertsListener implements EventListener is
    private field email: string
    private field message: string

    constructor EmailAlertsListener(email, message) is
        this.email = email
        this.message = message

    method update(filename) is
        system.email(email, replace('%s',filename,message))


// アプリケーションは、実行時にパブリッシャーとサブスクライバーを設定でき
// る。
class Application is
    method config() is
        editor = new Editor()

        logger = new LoggingListener(
            "/path/to/log.txt",
            "Someone has opened the file: %s")
        editor.events.subscribe("open", logger)

        emailAlerts = new EmailAlertsListener(
            "admin@example.com",
            "Someone has changed the file: %s")
        editor.events.subscribe("save", emailAlerts)

適応性

オブジェクトの状態の変更が他のオブジェクトの変更を必要とし 実際に影響を受けるオブジェクトの集りを事前に知ることができないか 影響を受けるオブジェクトが動的に変化する場合に Observer パターンを使用します

グラフィカル・ユーザー・インターフェースのクラスを扱う場合 このような問題によく遭遇します たとえば 特化したボタンのクラスを作成したとすると ユーザーがボタンを押すたびに起動する特別なコードをクライアントが差し込めるようにしたい場合です

Observer パターンでは サブスクリプション・インターフェースを実装したどんなオブジェクトでもパブリッシャー・オブジェクトのイベント通知を申し込めます 開発したボタンに通知申し込みの仕組みを加えることで サブスクライバー・クラスを通して 特別なコードをクライアントが差し込めるようにできます

アプリ内の一部のオブジェクトが他のオブジェクトを限定された期間だけ あるいは特別な場合にだけ監視しなければならない時 このパターンを使用してください

サブスクリプション・リストは動的で サブスクライバーは必要な時にいつでも参加または退去できます

実装方法

  1. ビジネス・ロジックを眺めて これを二つの部分に分けます 他のコードから独立した中核機能はパブリッシャーとして機能し 残りはサブスクライバー・クラスの集合となります

  2. サブスクライバー・インターフェースを宣言します 最低でも update メソッドを一つ宣言します

  3. パブリッシャー・インターフェースを宣言し サブスクライバー・オブジェクトをリストに追加と削除を行うメソッドのペアを書き入れます パブリッシャーはサブスクライバーとサブスクライバー・インターフェースを通してのみやりとりすることをお忘れなく

  4. どこに実際のサブスクリプション・リストを置くかを決め サブスクリプション用メソッドを実装します このコードは 通常すべてのパブリッシャー間で同じなので これを置くのに自明な場所は パブリッシャー・インターフェースから直接派生した抽象クラスです 具象クラスはそのクラスを拡張し サブスクリプションの振る舞いを継承します

    しかしながら 既存のクラス階層にこのパターンを適応する場合は 合成に基づく方法を検討してください つまり サブスクリプションのロジックを個別のオブジェクトに置き 実際の全パブリッシャーがそれを使うようにします

  5. パブリッシャーの具象クラスを作成します 何か重要なことがパブリッシャーの中で起きるたびに サブスクライバー全部に通知しなければいけません

  6. 具象サブスクライバー・クラスの中に更新通知メソッドを実装します 大多数のサブスクライバーは イベントについて何らかの周辺情報を必要とします それは 通知メソッドの引数として渡すことができます

    しかし もう一つのやり方があります 通知を受けたらすぐ サブスクライバーはそのデータを通知から直接取り込むことができます この場合 パブリッシャーはそれ自身を更新メソッドを通して渡します 少し柔軟性に欠けますが コンストラクターを通して パブリッシャーをサブスクライバーと永久にリンクすることもできます

  7. クライアントは 必要なすべてのサブスクライバーを作成し それらを適切なパブリッシャーに登録する必要があります

長所と短所

  • パブリッシャーのコードを変更することなく 新しいサブスクライバー・クラスを導入可能 パブリッシャー・インターフェースがある場合は 逆も可
  • 実行時にオブジェクト間の関係を確立できます
  • サブスクライバーへの通知の順番はランダムです

他のパターンとの関係

  • Chain of ResponsibilityCommandMediatorObserver リクエストの送り手と受け手を接続する様々な方法を示します

    • Chain of Responsibility 潜在的受け手の動的な連鎖に沿って どれか一つが処理するまで リクエストを順番に渡します
    • Command 送り手と受け手との間で単方向の接続を確立します
    • Mediator 送り手と受け手の間の直接の接続を削除し メディエーター・オブジェクトを介しての間接的通信を強制します
    • Observer では 受け手が動的にリクエストの受信申し込みをしたり 申し込み取り消しをしたりできます
  • MediatorObserver との違いは 理解に苦しむことがあります ほとんどの場合 これらのパターンのいずれかを実装すればいですが 両方を同時に適用することもできます どうすればそれができるか見てみましょう

    Mediator の主目的は システム構成コンポーネント間の相互依存をなくすことです その代わりに これらのコンポーネントは単一のメディエーター・オブジェクトに依存するようになります Observer の主目的は オブジェクト間の動的な単方向の接続を確立することにあり そこではあるオブジェクトが他のオブジェクトの部下として動作します

    Observer に依存した Mediator パターンの有名な実装方法があります メディエーター・オブジェクトがパブリッシャーとしての役割を果たし 他のコンポーネントがサブスクライバーとしてメディエーターのイベントに通知依頼をしたり依頼解除をします このように Mediator を実装すると Observer と非常に似たよう見えます

    混乱した時は Mediator パターンは 他の方法でも実装できるということを忘れないでください たとえば 同一のメディエーター・オブジェクトにすべてのコンポーネントを恒久的にリンクできます この実装方法は Observer には似ていませんが Mediator パターンの適用例の一つと言えます

    ここで プログラム中のすべてのコンポーネントがパブリッシャーとなり お互いに動的な接続が許された状況を想像してみてください ここでは 中心となるメディエーター・オブジェクトはなく 分散されたオブザーバーの集団があるだけです

コード例

Observer を C# で Observer を C++ で Observer を Go で Observer を Java で Observer を PHP で Observer を Python で Observer を Ruby で Observer を Rust で Observer を Swift で Observer を TypeScript で