怠惰系エンジニアのメモ帳

勉強した内容をメモしていきます。

Observerパターンメモ

Java本格入門」の中でデザインパターンについて触れられており、理解を深めるために実際にコーディングしながら読み進めたので、忘れないようにメモ。

認識に間違いがあれば指摘していただけると幸いです。

Observer パターンとは

Observer パターン(オブザーバ・パターン)とは、プログラム内のオブジェクトの状態を観察(英: observe)するようなプログラムで使われるデザインパターンの一種。出版-購読型モデルとも呼ばれる。暗黙的呼び出しの原則と関係が深い。 分散イベント処理システムの実装に主に使われる。言語によっては、このパターンで扱われる問題は言語が持つイベント処理構文で処理される。リアルタイムのアプリケーション配置の手段として興味深い機能である。

Observer パターン - Wikipedia

Wikipediaにも記載されているように、ObserverパターンはPublish(発行)-Subscribe(購読)パターンとも呼ばれるそうです。
個人的には、Publish-Subscribeパターンの方がしっくりきました。

Observerパターンで登場するクラス

Observerパターンで登場するのは、以下の2クラスがメイン。

  • Subject(or Publish)
    • Observerに対して通知を行うクラス。
    • 通知先となるObserverの登録(setObserver)・削除(deleteObserver)機能を提供する。
  • Observer(or Subscribe)
    • Subjectからの通知を受け、処理を行うクラス。
    • 通知を受ける update メソッドを提供する。

こんなシナリオを考えてみる

  1. ブロガーがブログに記事を投稿する。
  2. 記事が投稿されたら、購読者へ投稿通知が届く。

作成するクラスは以下のようになります。

  • Blogクラス - ブログ
    • Subjectにあたる。
  • Subscriberクラス - ブログの購読者
    • Observerにあたる。

コード

Subjectインターフェース

/**
 * <h1>Subjectインターフェース</h1>
 *
 * Observerの登録・削除・通知機能を提供します。<br/>
 *
 * @param <T> <code>getSubjectStatus</code>メソッドの戻り値の型
 */
public interface Subject<T> {

    /**
     * Observerを設定します。
     *
     * @param observe Observer
     */
    void setObserver(final Observer observe);

    /**
     * Observerを一括削除します。
     */
    void deleteObservers();

    /**
     * Observerを削除します。<br/>
     * このメソッドは、Observer側で呼び出されることを想定しています。
     *
     * @param observer 削除対象のObserver
     */
    void deleteObserver(Observer observer);

    /**
     * Observerに通知を行います。
     */
    void notifyObservers();

    /**
     * Subjectのステータスを取得します。
     *
     * @return ステータス
     */
    T getSubjectStatus();

    /**
     * 処理を実行します。<br/>
     * 登録されたObserverへ通知は、このメソッド内の処理で行われます。
     */
    void execute();

}

Observerインターフェース

**
 * 観察者(通知を受ける側)
 */
public interface Observer {

    /**
     * Subjectからの通知を受け、処理を行います。<br/>
     * <code>update</code>の呼び出しは、Subjectによって行われます。
     *
     * @param subject Subject
     */
    void update(Subject subject);
}

Blog(ブログ)クラス

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * <h1>ブログクラス</h1>
 *
 * 記事の投稿を行い、購読者へ通知を行います。
 *
 */
public class Blog implements Subject<String> {

    /** 通知先リスト */
    private final List<Observer> observers = new ArrayList<>();

    /** 最新記事のタイトル */
    private String latestTitle = "";

    @Override
    public void setObserver(final Observer observe) {
        this.observers.add(observe);
    }

    @Override
    public void deleteObservers() {
        this.observers.clear();
    }

    @Override
    public void deleteObserver(final Observer observer) {
        Objects.requireNonNull(observer, "削除対象のObserverがnullです。");
        this.observers.remove(observer);
    }

    @Override
    public void notifyObservers() {

        final int LOOP_SIZE = observers.size();
        for (int i = 0 ; i < LOOP_SIZE ; i++) {
            observers.get(i).update(this);
        }

    }

    /**
     * 最新記事のタイトルを取得します。
     *
     * @return 最新記事のタイトル。
     */
    @Override
    public String getSubjectStatus() {
        return this.latestTitle;
    }

    /**
     * 記事を投稿します。<br/>
     * 購読者(Observer)へ通知が行われます。
     */
    @Override
    public void execute() {
        System.out.println("記事を投稿しました。");
        this.notifyObservers();
    }

    /**
     * 記事のタイトルを設定します。
     *
     * @param title タイトル
     */
    public void setLatestTitle(final String title){
        this.latestTitle = title;
    }

    /**
     *
     * 購読者数を取得します。
     *
     * @return 購読者数
     */
    public int getSubscribeSize(){
        return this.observers.size();
    }
}

Subscriber(購読者)クラス

Subscriberクラスではあらかじめ購読回数を設定しておき、 上限に達した場合は通知が来ないように自身のインスタンスをSubjectから除外します。

import java.util.Objects;

/**
 * <h1>購読者クラス</h1>
 */
public class Subscriber implements Observer {

    /** ユーザ名 */
    private final String userName;

    /** 通知上限回数 */
    private final int notifyMaxCount;

    /** 通知回数 */
    private int notifyCount = 0;

    /**
     * コンストラクタ。<br/>
     *
     * ユーザ名が null の場合、<code>NullPointerException</code>がスローされます。
     *
     * @param userName ユーザ名
     * @param notifyMaxCount 通知上限回数
     */
    public Subscriber(final String userName, final int notifyMaxCount){
        Objects.requireNonNull(userName);
        this.userName = userName;
        this.notifyMaxCount = notifyMaxCount;
    }

    /**
     * 記事の投稿通知を受け取ります。<br/>
     * なお、通知回数が上限値に達した場合には、通知対象から自身を削除します。
     *
     * @param subject 購読先のBlogインスタンス
     */
    @Override
    public void update(final Subject subject) {
        System.out.printf("%sに「%s」の投稿通知が届きました。%n", this.userName, subject.getSubjectStatus());

        // 通知上限に達した場合には、通知対象から除外する。
        if(++notifyCount == notifyMaxCount){
            subject.deleteObserver(this);
        }
    }

    /**
     * ユーザ名を取得します。
     *
     * @return ユーザ名
     */
    public String getUserName(){
        return this.userName;
    }
}

シュミレート

public class Blogger {

    public static void main(String...args){

        // Subjectの生成
        Blog blog = new Blog();

        // Observerの生成
        Subscriber subscriber1 = new Subscriber("うさぎ", 2);
        Subscriber subscriber2 = new Subscriber("かめ", 1);

        // ObserverをSubjectへ登録
        blog.setObserver(subscriber1);
        blog.setObserver(subscriber2);

        // ブログへ記事を投稿
        blog.setLatestTitle("デザインパターンメモ");
        blog.execute();

        // ブログへ記事を投稿
        blog.setLatestTitle("SpringBootメモ");
        blog.execute();

    }

}

実行結果は以下の通りになります。

記事を投稿しました。
うさぎに「デザインパターンメモ」の投稿通知が届きました。
かめに「デザインパターンメモ」の投稿通知が届きました。
記事を投稿しました。
うさぎに「SpringBootメモ」の投稿通知が届きました。

まとめ

疑問

  • Publish-Subscribeパターン
    • Reactive Streamsって、Publish-Subscribeパターン?
  • Observerの削除について
    • 今回の例で、Observerの削除はObserver自身が行うようにしましたが、本来誰が削除するのが正しいのか…。