DP-19: Observer パターン
あるオブジェクトの状態変化(イベント)を、登録された複数のオブザーバーに自動的に通知するパターンです。 イベント発行者(Publisher)とリスナー(Observer)が疎結合になるため、 新しいリスナーを追加しても発行者側を変更する必要がありません。
Observer パターンとは
「ユーザー登録が完了したとき、ウェルカムメールを送り、ログを記録し、ポイントを付与する」 といった処理を実装する場合、これら全てをひとつのメソッドに書いてしまうと、 後から「クーポンも発行する」を追加するたびにそのメソッドを修正しなければなりません。 Observer パターンでは、「ユーザー登録が完了した」というイベントを発行する側(Publisher)と 「そのイベントに反応する」側(Observer)を分離します。 発行者はリスナーの詳細を知らず、リスナーは発行者の詳細を知らない状態で連携できます。
Push型 vs Pull型
| 方式 | 仕組み | メリット | デメリット |
|---|---|---|---|
| Push型 | Publisher がイベントデータをオブザーバーに送る | オブザーバーが追加のデータ取得をしなくて良い | 不要なデータまで送られることがある |
| Pull型 | Publisher は変化があったことのみ通知し、オブザーバーが必要なデータを取得する | オブザーバーが必要な情報だけ取得できる | オブザーバーが Publisher への参照を保持する必要がある |
Java 標準ライブラリの実例: java.beans.PropertyChangeListener
Java 9 以降、java.util.Observer とjava.util.Observable は非推奨(deprecated)となりました。 代わりに java.beans.PropertyChangeListener の使用が推奨されています。
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
class UserModel {
private final PropertyChangeSupport support = new PropertyChangeSupport(this);
private String name;
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void setName(String newName) {
String oldName = this.name;
this.name = newName;
// プロパティ変更を全リスナーに通知する
support.firePropertyChange("name", oldName, newName);
}
}
// 使用例
UserModel user = new UserModel();
user.addPropertyChangeListener(evt ->
System.out.println("名前が変わりました: " + evt.getOldValue() + " → " + evt.getNewValue())
);
user.setName("山田太郎"); // → 「名前が変わりました: null → 山田太郎」サンプルコード
Java 8 版では EventListener インターフェースと EventPublisher クラスで Observer パターンを実装しています。subscribe/unsubscribe でリスナーを動的に管理でき、publish を呼ぶと登録済みの全リスナーに通知が届きます。
よくあるミス・注意点
⚠️ unsubscribe(登録解除)を忘れてメモリリークが発生する
Observer パターン最大の落とし穴です。 Publisher がオブザーバーへの参照を保持し続けるため、unsubscribe() を呼ばずに オブザーバーオブジェクトを「捨てた」つもりになっても、 Publisher からの参照が残るためガベージコレクションで回収されません。 画面遷移やオブジェクトのライフサイクル終了時には必ず登録解除しましょう。
⚠️ イベントの連鎖(Cascading)で無限ループになる
オブザーバーが通知を受けた際に、同じ Publisher に対して新たなイベントを発行すると、 そのイベントがまた同じオブザーバーに届き、無限ループになる場合があります。 イベントハンドラ内でイベントを発行する場合は、ループの終了条件を必ず設けましょう。
⚠️ java.util.Observer / java.util.Observable を使う(Java 9+ で非推奨)
古い書籍やコードには java.util.Observer とjava.util.Observable を使った例が載っていますが、 Java 9 で非推奨(deprecated)になりました。 新しいコードでは java.beans.PropertyChangeListener か、 このサンプルのように独自のインターフェースを定義することをお勧めします。
テストする観点
- publish したイベントが、登録済みの全オブザーバーに届くこと(3つ登録したら3つ全員に通知されること)
- unsubscribe 後は、解除したオブザーバーにイベントが届かないこと
- オブザーバーが0件の状態で publish しても例外が発生しないこと(境界値)
- 同じオブザーバーを複数回 subscribe すると複数回通知されること(または1回のみにすること)を確認する
- Java 17 版: Event record の type() と data() が正しい値を返すこと
- Java 21 版: LogListener の switch 式が各 AppEvent サブタイプに対して正しいメッセージを生成すること
- Java 21 版: UserRegistered・OrderPlaced・PaymentFailed それぞれを publish したとき、EmailNotificationListener が正しく反応すること