広告

Java : Observer パターン (図解/デザインパターン)

Observer パターンとは、GoF によって定義されたデザインパターンの1つです。
イベントを不特定多数に通知できます。依存関係の向きを逆転させるテクニックとしても使えますね。

本記事では、Observer パターンを Java のコード付きで解説していきます。


デザインパターン(GoF) 関連記事

概要

Observer パターン(オブザーバー・パターン)とは、プログラム内のオブジェクトに関するイベント(事象)を他のオブジェクトへ通知する処理で使われるデザインパターンの一種。

Observer パターンとは、

  • イベントを通知するクラスがあり
  • そのクラスの 変更なし で、イベントを受け取りたいクラスを増やす(拡張する)

という目的のための設計です。

もう少し簡単にいうと、

  • イベントを 不特定多数 のクラスに通知する

という設計です。

また、依存関係の 向きを逆転 させるテクニックとしても使えます。

Observer は、日本語的に発音すると「オブザーバー」となります。
意味は「監視者」ですね。


【 Observer パターンのイメージ図 】

ユースケース図1

ユースケース図2

クラスA、クラスB、クラスC ... と、イベントを受け取るクラスはいくらでも 拡張 できるのがメリットです。
しかも、イベントを通知する側のクラスは 修正なし で OK です。

いわゆる 開放閉鎖の原則

  • ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。

ですね。

もしかしたら、Observer という名前より

  • リスナー (Listener)
  • コールバック (Callback)

という名前のほうが聞き覚えがあるかもしれません。
(特に Swing や Android などの経験があるかた)

リスナーやコールバック、Observer パターンは、使い方に多少の違いはあるとは思います。
ただ、根幹にある大きな目的は、すべて同じと考えてよいのかな…と思います。


【注意】

本記事のコード例ではサブスレッドは使っていません。

Observer パターンでは、

  1. サブスレッドで処理を実行
  2. 処理が完了したら通知(notify) する

というパターンもよく出てきます。
その場合、排他や同期といったことも考慮する必要があります。

関連記事:


クラス図とシーケンス図

クラス図1

シーケンス図1

上の図は、Observer パターンの一般的なクラス図とシーケンス図です。

  • Subject はイベントを 通知(notify) する側
  • ConcreteObserver はイベントを 受信(update) する側

となります。

このクラス図をそのままコードにしてみましょう。

public interface Observer {
    void update();
}

public class ConcreteObserverA implements Observer {
    @Override
    public void update() {
        System.out.println("ObserverA update!");
    }
}

public class ConcreteObserverB implements Observer {
    @Override
    public void update() {
        System.out.println("ObserverB update!");
    }
}
public class Subject {

    private final List<Observer> observers = new ArrayList<>();

    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    public void notifyObservers() {
        for (final var observer : observers) {
            observer.update();
        }
    }
}

次に Subject と ConcreteObserver を使う例です。

final var subject = new Subject();

final var a = new ConcreteObserverA();
subject.addObserver(a);

final var b = new ConcreteObserverB();
subject.addObserver(b);

System.out.println("-- notify --");
subject.notifyObservers();

// 結果
// ↓
//-- notify --
//ObserverA update!
//ObserverB update!

// ConcreteObserverA だけ削除
subject.removeObserver(a);

System.out.println("-- notify --");
subject.notifyObservers();

// 結果
// ↓
//-- notify --
//ObserverB update!

コード例

もう少し具体的な例も見てみましょう。

とてもシンプルな Chat クラスを考えてみます。
このクラスは Observer パターンの Subject に相当します。

クラス図2

シンプルな Chat クラスは、最新のメッセージ1件のみを保持します。

  • postMessage メソッド : メッセージをポスト
  • getMessage メソッド : 最新のメッセージを取得
public class Chat {
    private String message = "";

    public void postMessage(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}

Chat クラスを使うコード例です。

final var chat = new Chat();

chat.postMessage("Hello");
System.out.println(chat.getMessage()); // Hello

chat.postMessage("Thanks!");
System.out.println(chat.getMessage()); // Thanks!

chat.postMessage("See you");
System.out.println(chat.getMessage()); // See you

Observer パターンを使わないケース

まずは Observer パターンを使わないケースを考えてみます。

メッセージが Chat.postMessage されるたびに、メッセージをコンソールへと出力する

  • MessagePrinter クラス

を追加してみましょう。
メッセージは連番付きで表示させます。

クラス図3

public class MessagePrinter {
    private int count;

    public void print(String message) {
        count++;
        System.out.println(count + " : " + message);
    }
}

この MessagePrinter クラスを Chat クラスに組み込みます。

public class Chat {
    private final MessagePrinter printer;
    private String message = "";

    public Chat(MessagePrinter printer) {
        this.printer = printer;
    }

    public void postMessage(String message) {
        this.message = message;

        printer.print(message); // ★ポイント
    }

    public String getMessage() {
        return message;
    }
}

それでは、MessagePrinter を組み込んだ Chat クラスを使ってみましょう。

final var chat = new Chat(new MessagePrinter());

chat.postMessage("Hello");

// 結果
// ↓
//1 : Hello

chat.postMessage("Thanks!");

// 結果
// ↓
//2 : Thanks!

chat.postMessage("See you");

// 結果
// ↓
//3 : See you

メッセージが Chat.postMessage されるたびに、コンソールに連番付きでメッセージが表示されました。
問題なく機能していますね。

さて、しばらくして Chat クラスに機能追加の依頼がありました。
Chat のメッセージをファイルにログとして保存したい、とのことです。

そこで ChatLogger クラスを追加することにしました。
ChatLogger.write メソッドでは、ファイルにメッセージを追記(APPEND) していきます。

クラス図4

public class ChatLogger {
    private final Path target;

    public ChatLogger(Path target) {
        this.target = target;
    }

    public void write(String message) {
        try {
            Files.writeString(target, message,
                    StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        } catch (IOException e) {
            System.out.println("IOException!");
        }
    }
}

この ChatLogger クラスを Chat クラスに組み込みます。

public class Chat {
    private final MessagePrinter printer;
    private final ChatLogger logger;
    private String message = "";

    public Chat(MessagePrinter printer, ChatLogger logger) {
        this.printer = printer;
        this.logger = logger;
    }

    public void postMessage(String message) {
        this.message = message;

        printer.print(message);
        logger.write(message); // ★ポイント
    }

    public String getMessage() {
        return message;
    }
}

それでは、ChatLogger クラスを組み込んだ Chat クラスを使ってみましょう。
ログは chat.log ファイルに保存します。

final var logFile = Path.of("R:", "java-work", "chat.log");
System.out.println(logFile); // R:\java-work\chat.log

final var chat = new Chat(new MessagePrinter(), new ChatLogger(logFile));

System.out.println("-- post messages --");
chat.postMessage("Hello");
chat.postMessage("Thanks!");
chat.postMessage("See you");

System.out.println("-- chat.log --");
System.out.println(Files.readString(logFile));

// 結果
// ↓
//-- post messages --
//1 : Hello
//2 : Thanks!
//3 : See you
//-- chat.log --
//Hello
//Thanks!
//See you

問題なしですね。

しかし、機能追加のたびに Chat クラスを修正するのは、あまり汎用的とはいえません。

チャットなので、いずれはネットワーク経由でメッセージを通知する機能も必要になるでしょう。
すると、また Chat クラスに修正が…

これは 開放閉鎖の原則

  • ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。

に反しています。

この Chat クラスは 修正に対して閉じていない ということですね。

今回の例はとてもシンプルなので、そこまで問題に感じないかもしれません。
しかし、実際の開発現場では、もっと複雑なケースもあることでしょう。

クラスはなるべく小さく小さく、機能もシンプルに(単一責任の原則) というのを心がけたいですね。


Observer パターンを使うケース

次は、Observer パターン を使う例を見てみましょう。

先ほど Observer パターンを使わないケース で作成した

  • MessagePrinter
  • ChatLogger

の2つのクラスは、Observer パターンの ConcreteObserver に相当します。
Chat クラスは Subject に相当します。

クラス図5

まずは Observer インタフェースです。
update メソッドでは、最新のメッセージ(String) をパラメータで通知します。

public interface Observer {
    void update(String message);
}

次に、MessagePrinter と ChatLogger クラスに Observer インタフェースを実装(implements) します。

public class MessagePrinter implements Observer {
    private int count;

    @Override
    public void update(String message) {
        count++;
        System.out.println(count + " : " + message);
    }
}

public class ChatLogger implements Observer {
    private final Path target;

    public ChatLogger(Path target) {
        this.target = target;
    }

    @Override
    public void update(String message) {
        try {
            Files.writeString(target, message + System.lineSeparator(),
                    StandardOpenOption.CREATE, StandardOpenOption.APPEND);
        } catch (IOException e) {
            System.out.println("IOException!");
        }
    }
}

最後に Chat クラスです。

  • addObserver メソッド : Observer を追加
  • notifyObservers メソッド : Observer にメッセージを通知

(今回、removeObserver メソッドは使わないので省いています)

public class Chat {
    private final List<Observer> observers = new ArrayList<>();
    private String message = "";

    // ★ポイント
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    public void postMessage(String message) {
        this.message = message;

        notifyObservers(); // ★ポイント
    }

    public String getMessage() {
        return message;
    }

    // ★ポイント
    private void notifyObservers() {
        for (final var observer : observers) {
            observer.update(message);
        }
    }
}

それでは、Observer パターンに対応した Chat クラスを使ってみましょう。

final var logFile = Path.of("R:", "java-work", "chat.log");
System.out.println(logFile); // R:\java-work\chat.log

final var chat = new Chat();
chat.addObserver(new MessagePrinter());
chat.addObserver(new ChatLogger(logFile));

System.out.println("-- post messages --");
chat.postMessage("Hello");
chat.postMessage("Thanks!");
chat.postMessage("See you");

System.out.println("-- chat.log --");
System.out.println(Files.readString(logFile));

// 結果
// ↓
//-- post messages --
//1 : Hello
//2 : Thanks!
//3 : See you
//-- chat.log --
//Hello
//Thanks!
//See you

結果も問題なしですね。

Observer パターンを使うと、Chat クラスの 修正なし で、機能(Observer) を追加(拡張) できるのが分かりますでしょうか。
例えば、ネットワーク経由のメッセージ通知機能が必要になっても、Chat クラスの修正なしで対応できます。

開放閉鎖の原則

  • ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。

になっていますね。

Observer パターンを使った Chat クラスは、

  • Observer による機能の拡張が可能 (開いている)
  • Chat クラス自体の修正は必要なし (閉じている)

となります。

補足

このクラスは、Observableオブジェクト、つまりモデル/ビュー・パラダイムの「データ」を表します。

Java の標準API には、

  • Observable クラス
  • Observer インタフェース

が用意されています。

ただし、これらの API には問題があったため Java 9 から 非推奨 となりました。
詳細は引用先のドキュメントをご参照ください。

まとめ

Observer パターンとは、

  • イベントを 不特定多数 のクラスに通知する

という設計です。

また、依存関係の 向きを逆転 させるテクニックとしても使えます。

汎用性があってとても有用なパターンだと思います。
ぜひ有効に活用していきたいですね。


関連記事

ページの先頭へ