Java : Observer パターン (図解/デザインパターン)
Observer パターンとは、GoF によって定義されたデザインパターンの1つです。
イベントを不特定多数に通知できます。依存関係の向きを逆転させるテクニックとしても使えますね。
本記事では、Observer パターンを Java のコード付きで解説していきます。
デザインパターン(GoF) 関連記事
- 生成に関するパターン
- 振る舞いに関するパターン
概要
Observer パターンとは、
- イベントを通知するクラスがあり
- そのクラスの 変更なし で、イベントを受け取りたいクラスを増やす(拡張する)
という目的のための設計です。
もう少し簡単にいうと、
- イベントを 不特定多数 のクラスに通知する
という設計です。
また、依存関係の 向きを逆転 させるテクニックとしても使えます。
Observer は、日本語的に発音すると「オブザーバー」となります。
意味は「監視者」ですね。
【 Observer パターンのイメージ図 】
クラスA、クラスB、クラスC ... と、イベントを受け取るクラスはいくらでも 拡張 できるのがメリットです。
しかも、イベントを通知する側のクラスは 修正なし で OK です。
いわゆる 開放閉鎖の原則
- ソフトウェア要素(クラス、モジュール、関数など)は、拡張に対しては開いており、修正に対しては閉じているべきである。
ですね。
もしかしたら、Observer という名前より
- リスナー (Listener)
- コールバック (Callback)
という名前のほうが聞き覚えがあるかもしれません。
(特に Swing や Android などの経験があるかた)
リスナーやコールバック、Observer パターンは、使い方に多少の違いはあるとは思います。
ただ、根幹にある大きな目的は、すべて同じと考えてよいのかな…と思います。
【注意】
本記事のコード例ではサブスレッドは使っていません。
Observer パターンでは、
- サブスレッドで処理を実行
- 処理が完了したら通知(notify) する
というパターンもよく出てきます。
その場合、排他や同期といったことも考慮する必要があります。
関連記事:
クラス図とシーケンス図
上の図は、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 に相当します。
シンプルな 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 クラス
を追加してみましょう。
メッセージは連番付きで表示させます。
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) していきます。
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 に相当します。
まずは 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 クラス自体の修正は必要なし (閉じている)
となります。
補足
Java の標準API には、
- Observable クラス
- Observer インタフェース
が用意されています。
ただし、これらの API には問題があったため Java 9 から 非推奨 となりました。
詳細は引用先のドキュメントをご参照ください。
まとめ
Observer パターンとは、
- イベントを 不特定多数 のクラスに通知する
という設計です。
また、依存関係の 向きを逆転 させるテクニックとしても使えます。
汎用性があってとても有用なパターンだと思います。
ぜひ有効に活用していきたいですね。
関連記事
- 標準APIにならう命名規則
- コメントが少なくて済むコードを目指そう
- シングルトン・パターンの乱用はやめよう
- メソッドのパラメータ(引数)は使う側でチェックしよう
- 不変オブジェクト(イミュータブル) とは
- 依存性の注入(DI)をもっと気軽に
- 不要になったコードはコメントアウトで残さずに削除しよう
- 簡易的な Builder パターン
- 読み取り専用(const) のインタフェースを作る
- 図解/デザインパターン一覧 (GoF)
- Abstract Factory パターン
- Adapter パターン
- Bridge パターン
- Builder パターン
- Chain of Responsibility パターン
- Command パターン
- Composite パターン
- Decorator パターン
- Facade パターン
- Factory Method パターン
- Flyweight パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン
- Memento パターン
- Prototype パターン
- Proxy パターン
- Singleton パターン
- State パターン
- Strategy パターン
- Template Method パターン
- Visitor パターン