「Observer」パターンとは?サンプルを踏まえてわかりやすく解説!【Java】

  • LINEで送る
「Observer」パターンとは?サンプルを踏まえてわかりやすく解説!

 本記事ではGoFのデザインパターンのプログラムの振る舞いに関するパターンの一つである「Observer」パターンを解説します。このパターンを一言で説明するならば、「観察対象のオブジェクトの状態が変化したとき、観察者のオブジェクトに通知を行う方法」と言えるでしょう。「Observer」パターンをサンプルを踏まえて解説します。

Observerパターンとは

 Observerパターンは、あるオブジェクト(Subject)の状態が変化したときに、それを監視している複数のオブジェクト(Observer)に自動で通知を行う仕組みを提供するデザインパターンです。Observerとは「観察者」という意味がありますが、どちらかというと「通知」に重きが置かれます。通知を行う仕組みの方に設計と制御の中心があります。ただしObserver側にも通知を受けてその「変化にどのように対応するか」を設計することも非常に重要になります。

 例えば、ECサイトで商品を購入した時のことを想像してみてください。宅配便として荷物が届く時に、今どこに宅配便が届いているかを、毎度毎度宅配会社に問い合わせることはあるでしょうか。おそらくそんなことはありません。メールだったり、アプリだったり、Webサイト等で確認するのではないでしょうか。この仕組みがObserverパターンに類似しています。「配送センター(Subject)」が「通知アプリ(Observer)」に現在の宅配便の位置(ステータス)を通知しています。

Observerパターンの構成要素

 Observerパターンの構成要素は非常にシンプルです。このパータンには主に2つのインターフェースと、そのインターフェースを実装した2つのクラスがあります。

要素役割
Subject(被観察者)・状態を持ち、状態の変更時にObserverへ通知を行う
・Observerの登録・解除を行う
※インターフェース
Observer(観察者)状態の変化に応じた処理を実装する
※インターフェース
ConcreteSubject実際のステータスを管理する
※Subjectを実装したクラス
ConcreteObserver通知を受けて処理を行うクラス
※Observerを実装したクラス

Observerパターンの書き方

  先ほどの宅配便の例で実際にコードを書いて解説していきます。

❌ Observerを使わない場合

 まずはObserverパターンを利用しなかった場合、どのようなことが起こるでしょうか。それは配送を管理するクラスが、通知先のサービスを直接呼び出す必要があります。コード例を紹介します。

public class DeliveryTracker {
    private String status;

    public void setStatus(String status) {
        this.status = status;
        notifyByEmail(status);
        notifyByApp(status);
        notifyBySMS(status); // 通知手段が増えるたびに追加
    }

    private void notifyByEmail(String status) {
        System.out.println("【メール通知】配送状況:" + status);
    }

    private void notifyByApp(String status) {
        System.out.println("【アプリ通知】配送状況:" + status);
    }

    private void notifyBySMS(String status) {
        System.out.println("【SMS通知】配送状況:" + status);
    }
}

 このDeliveryTrackerは配送ステータスを持っています。このステータスを上書きしていくことで、現在のステータスを取得できます。ここでステータスをsetStatus()で更新します。このメソッド内では、ステータスを更新したときに色々な場所に通知するためのメソッドを追加しています。

 では配送ステータスを別の通知方法で通知する場合(Webサイト上で通知)、どのような実装にする必要があるでしょうか。それは新たにメソッドを追加する必要があります。

    :
    :
    public void setStatus(String status) {
        this.status = status;
        notifyByEmail(status);
        notifyByApp(status);
        notifyBySMS(status);
        notifyByWeb(status);
    }

    private void notifyByWeb(String status) {
        System.out.println("【Web通知】配送状況:" + status);
    }
    :
    :

 上記のように、DeliveryTrackerに新たに専用のメソッドと、setStatus()内でのメソッド実行処理を追加していく必要が出てきます。これには以下の問題点が挙げられます。

問題点説明
クラスの密結合DeliveryTrackerがすべての通知手段に直接依存するため、通知手段の変更が困難になる。
拡張性の欠如新しい通知手段(例:LINE、Slackなど)を追加するたびにDeliveryTrackerのコードを書き換える必要がある。
再利用性が低い通知処理が個別に切り出されていないため、他のシステムでも再利用しづらい。
変更に弱い設計例えばSMS通知の仕様が変わった場合も、DeliveryTrackerの修正が必要になるため影響範囲が広い。

 Observerを使う場合

 これらの問題点を踏まえて、疎結合で再利用性が高くなるようにObserverパターンを適用していきます。まずはSubjectインターフェースを定義します。本例ではDeliverySubjectとしています。

public interface DeliverySubject {
    void addObserver(DeliveryObserver o);
    void removeObserver(DeliveryObserver o);
    void notifyObservers();
}

 それぞれのメソッドについては下記になります。

メソッド役割
addObserver(DeliveryObserver o)引数で受け取ったObserverを通知先のリストに追加する
removeObserver(DeliveryObserver o)引数で受け取ったObserverを通知先のリストから削除する
notifyObservers()ステータスを更新して通知する

 次にObserverの通知の仕組みを統一し、一括で通知を行うためのObserverのインターフェースを定義します。本例ではDeliveryObserverとしています。

public interface DeliveryObserver {
    void update(DeliveryStatus status);
}

 update()メソッドでステータスが更新されて通知が来た時の処理を実装します。

 これらのインターフェースを踏まえて、DeliverySubjectのインターフェースを実装してDeliveryTrackerの処理を書いていきます。

public class DeliveryTracker implements DeliverySubject {

    private List<DeliveryObserver> observers = new ArrayList<>();
    private DeliveryStatus deliveryStatus;

    public void setDeliveryStatus(DeliveryStatus status) {
        this.deliveryStatus = status;
        notifyObservers();
    }

    @Override
    public void notifyObservers() {
        for (DeliveryObserver observer : observers) {
            observer.update(deliveryStatus);
        }
    }

    @Override
    public void addObserver(DeliveryObserver o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(DeliveryObserver o) {
        observers.remove(o);
    }
}

 DeliveryTrackerは2つのフィールドを持っています。

フィールド名役割
List<DeliveryObserver> observersObserverである通知先をListで管理するためのフィールド
DeliveryStatus deliveryStatus・配送ステータスを表すEnumのフィールド
・Enumにする理由は、文字列に比べて意味合いを特定しやすく、文字列のように想定されていない文字が含まれるのを防ぐため
・Enumの定義
READY_FOR_SHIPMENT, // 出荷準備中
SHIPPED, // 出荷済み
IN_TRANSIT, // 配達中
DELIVERED, // 配達完了
FAILED // 配達失敗

 notifyObservers内ではobserversのList内のDeliveryObserverのupdate()メソッドを実行しています。すべてのObserverが1つのインタフェースを実装していることで、DeliveryTrackerが個々のObserverに依存することなく、ステータスの更新通知を送ることができます。

 次に、DeliveryObserverを実装して、複数の通知先を用意します。通知先は下記4個とします。またそれぞれのクラス名についても記載します。

  • アプリケーションでの通知 → AppNotifier
  • Emailでの通知 → EmailNotifier
  • LINEでの通知 → LineNotifier
  • Webページからの通知 → WebNotifier
// アプリケーションでの通知
public class AppNotifier implements DeliveryObserver {
    @Override
    public void update(DeliveryStatus status) {
        System.out.println(String.format("[アプリ通知]配送ステータスが更新されました: %s", status));
    }
}

// Emailでの通知
public class EmailNotifier implements DeliveryObserver {
    @Override
    public void update(DeliveryStatus status) {
        System.out.println(String.format("[メール通知]配送ステータスが更新されました: %s", status));
    }
}

// LINEでの通知
public class LineNotifier implements DeliveryObserver {
    @Override
    public void update(DeliveryStatus status) {
        System.out.println(String.format("[LINE通知]配送ステータスが更新されました: %s", status));

    }
}

// Webページからの通知
public class WebNotifier implements DeliveryObserver {
    @Override
    public void update(DeliveryStatus status) {
        // Web通知の実装
        System.out.println(String.format("[Web通知]配送ステータスが更新されました: %s", status));
    }
}

 本例では、標準出力でシンプルな文字列のみを出力していますが、本来はそれぞれのステータス変化に伴って、そのObserverに適した処理を実装します。

 では実際にこれらのSubjectとObserverを利用してそれぞれの通知先にステータスの更新を通知します。

public class ObserverSample {
    public static void main(String[] args) {
        System.out.println("Observer Pattern Sample");

        // 配送通知システムを担うObjectを生成
        DeliveryTracker deliveryTracker = new DeliveryTracker();

        // それぞれの配送方法を生成
        DeliveryObserver appNotifier = new AppNotifier();
        DeliveryObserver emailNotifier = new EmailNotifier();
        DeliveryObserver lineNotifier = new LineNotifier();
        DeliveryObserver webNotifier = new WebNotifier();

        // 通知先を追加
        deliveryTracker.addObserver(appNotifier);
        deliveryTracker.addObserver(emailNotifier);
        deliveryTracker.addObserver(lineNotifier);
        deliveryTracker.addObserver(webNotifier);

        // 配送ステータスを更新
        deliveryTracker.setDeliveryStatus(DeliveryStatus.READY_FOR_SHIPMENT);
        
        sepa();

        // 通知先を削除
        deliveryTracker.removeObserver(webNotifier);
        deliveryTracker.setDeliveryStatus(DeliveryStatus.READY_FOR_SHIPMENT);

        sepa();
        
        // 配送ステータスを更新
        deliveryTracker.setDeliveryStatus(DeliveryStatus.DELIVERED);
    }
    private static void sepa() {
        System.out.println("===================================================================");
    }
}
>> 実行結果
[アプリ通知]配送ステータスが更新されました: READY_FOR_SHIPMENT
[メール通知]配送ステータスが更新されました: READY_FOR_SHIPMENT
[LINE通知]配送ステータスが更新されました: READY_FOR_SHIPMENT
[Web通知]配送ステータスが更新されました: READY_FOR_SHIPMENT
===================================================================
[アプリ通知]配送ステータスが更新されました: READY_FOR_SHIPMENT
[メール通知]配送ステータスが更新されました: READY_FOR_SHIPMENT
[LINE通知]配送ステータスが更新されました: READY_FOR_SHIPMENT
===================================================================
[アプリ通知]配送ステータスが更新されました: DELIVERED
[メール通知]配送ステータスが更新されました: DELIVERED
[LINE通知]配送ステータスが更新されました: DELIVERED

 まずはDeliveryTrackerのインスタンスを生成します。次にそれぞれの通知先のインスタンスを生成した後、addObserver()で通知先リストに追加します。ここで配送ステータスをREADY_FOR_SHIPMENTに更新するとそれぞれの通知先で処理が実行されます。

 さらに、通知先の消去と、再度配送ステータスをDELIVEREDに変更すると、登録済みの通知先にのみ配送ステータスが更新された時の処理が走ります。

メリットとデメリット

メリット

メリット説明
ObserverとSubjectが疎結合SubjectとObserverが直接依存しないため、柔軟で再利用しやすい設計が可能
Observerの拡張性が高い新しいObserverを追加するだけで機能を拡張できる。Subject側の変更は不要。
通知を自動化できる状態変化をObserverに自動で通知するため、監視の手間が省ける。

デメリット

デメリット説明
処理の順序が不明確になることがある複数のObserverに通知される順番が保証されないため、順序依存の処理には注意が必要。
デバッグが難しくなることがあるObserver同士で関係がある場合は依存関係が見えにくいため、挙動を追うのが難しい場合がある。

 Observerパターンには上記のメリットとデメリットがあります。これらのメリットとデメリットからObserverパターンが有効なシチュエーションには下記のような場合があります。

  • GUIアプリ、フォーム送信、ゲームのイベント処理等でイベント(ボタンが押された時等)が発生した時に、ログ記録/通知送信/UI更新を同時に行う場合
    • イベント発生源(Subject)と反応する処理(Observer)を疎結合で分離できる
  • 配送ステータスの変更を、アプリ・メール・LINEに同時反映する場合
    • データ(Subject)が1箇所で変化すると、全ての依存先(Observer)に即時反映できる
  • サーバーの障害を検知して、Slack/メール/アラート装置などに通知する場合
    • 通知処理をObserverとしてモジュール化することで再利用可能
    • Subject側の設計を一切変えずに、新しい通知チャネルを追加可能。

まとめ

 Observerパターンは、「変化を知りたい」オブジェクトがある場合に非常に有効な手段です。状態を一元管理しながらも、関係する複数のオブジェクトに通知できる仕組みは、UIやリアルタイムなシステムにおいて重宝されます。

 本記事で利用したサンプルケースのクラス図は下記になります。

Observerパターンのクラス図

 最後に「Observer」パターンとは「観察対象のオブジェクトの状態が変化したとき、観察者のオブジェクトに通知を行う方法」です。うまく使っていきましょう。

 本記事で利用したコードは下記のgit上で公開しております。

https://github.com/tamotech1028/observer

最新の投稿

SNSでもご購読できます。

コメント

コメントを残す