『Head Firstデザインパターン』 Observerパターン

Observerパターンとは

Observerパターンは、オブジェクト間の1対多の依存関係を定義し、あるオブジェクトの状態が変化するとそれに依存しているすべてのオブジェクトが自動的に通知され更新されるようにする。

ObserverパターンではPublisherをサブジェクトと呼び、Subscribeをオブザーバと呼ぶ。 サブジェクトは状態を保持し制御(支配)するオブジェクト。一方で、オブザーバは状態を持たないが状態を使用できる。 オブザーバは多数存在しサブジェクトの状態が変化した際に通知してもらう。 Observerパターンはサブジェクトとオブザーバが疎結合となるオブジェクト設計を提供する。

疎結合設計の威力

  1. サブジェクトがオブザーバについて知っているのはObserverインターフェースを実装しているということだけ。
    インターフェースに依存しているので、サブジェクトはオブザーバについて具体的なことを知ることは一切ない。

  2. 新しいオブザーバクラスを作ってもサブジェクトクラスを変更する必要がない。
    Observerインターフェースを実装した新しいクラスを作るだけでよい。

  3. サブジェクト・オブザーバのどちらを変更しても他方に影響を与えない
    お互いがインターフェースを満たしている限り、自由に変更できる。

設計原則

相互にやり取りするオブジェクト間には、疎結合設計を使用するようにする。

動き

<?php
// サブジェクト
$weatherData = new WeatherData();
// オブザーバにサブジェクトを注入
$currentDisplay = new CurrentConditionDisplay($weatherData);
$statisticsDisplay = new StatisticsDisplay($weatherData);
// サブジェクトの状態が更新されると連動してサブジェクトに登録されたオブザーバに通知が送られる
$weatherData->setMeasurements(27.0, 65, 30.4);
// オブザーバの削除
$weatherData->removeObserver($currentDisplay);
$weatherData->notifyObservers();

この場合、サブジェクトはWeatherDataであり、CurrentConditionDisplayやStatisticsDisplayの表示要素がオブザーバである。

<?php

namespace Observer;

class CurrentConditionDisplay implements Observer, DisplayElement
{
    private float $temperature;
    private float $humidity;
    private Subject $weatherData;

    /**
     * CurrentConditionDisplay constructor.
     * @param Subject $weatherData
     */
    public function __construct(Subject $weatherData)
    {
        $this->weatherData = $weatherData;
        $weatherData->registerObserver($this);
    }

    public function display(): void
    {
        echo "現在の気象状況:温度{$this->temperature}度 湿度{$this->humidity}%\n";
    }

    public function update(float $temperature, float $humidity, float $pressure): void
    {
        $this->temperature = $temperature;
        $this->humidity = $humidity;
        $this->display();
    }
}

オブザーバは表示要素に必要な情報を取得するためにコンストラクタでWeatherDataサブジェクトを登録する。

<?php
namespace Observer;
/**
 * Class WeatherData
 */
class WeatherData implements Subject
{
    /**
     * @var Observer[]
     */
    private array $observers;
    /**
     * @var float
     */
    private float $temperature;
    /**
     * @var float
     */
    private float $humidity;
    /**
     * @var float
     */
    private float $pressure;
    /**
     * WeatherData constructor.
     */
    public function __construct()
    {
        $this->observers = [];
    }
    /**
     * WeatherDataの値の変化をオブザーバに伝えるためサブジェクトに登録する
     * @param Observer $observer
     */
    public function registerObserver(Observer $observer): void
    {
        $this->observers[] = $observer;
    }
    /**
     * @param Observer $observer
     */
    public function removeObserver(Observer $observer): void
    {
        foreach ($this->observers as $index => $observers) {
            if ($observers === $observer) {
                unset($this->observers[$index]);
            }
        }
    }
    /**
     * 気候状態をセットする
     * オブザーバを登録した状態で気候状態をセットするとオブザーバに通知される
     * @param float $temperature
     * @param float $humidity
     * @param float $pressure
     */
    public function setMeasurements(float $temperature, float $humidity, float $pressure): void
    {
        $this->temperature = $temperature;
        $this->humidity = $humidity;
        $this->pressure = $pressure;
        $this->measurementsChanged();
    }
    /**
     * 更新された設定値が設定されたら通知を送る
     */
    private function measurementsChanged(): void
    {
        $this->notifyObservers();
    }
    /**
     * オブザーバに通知する
     */
    public function notifyObservers(): void
    {
        foreach ($this->observers as $observer) {
            $observer->update($this->temperature, $this->humidity, $this->pressure);
        }
    }
}

測定値が変化する(setMeasurementsメソッド)とWeatherDataに登録されたオブザーバのupdateメソッドを使用してオブザーバ側で必要な処理を行う。

PHPSplSubjectSplObserverを使用した実装

Standard PHP LibraryのSplSubjectとSplObserverインターフェースを用いればObserverパターンを実装できる。

/**
 * The <b>SplSubject</b> interface is used alongside
 * <b>SplObserver</b> to implement the Observer Design Pattern.
 * @link https://php.net/manual/en/class.splsubject.php
 */
interface SplSubject  {

        /**
         * Attach an SplObserver
         * @link https://php.net/manual/en/splsubject.attach.php
         * @param SplObserver $observer <p>
     * The <b>SplObserver</b> to attach.
         * </p>
         * @return void 
         * @since 5.1.0
         */
        public function attach (SplObserver $observer);

        /**
         * Detach an observer
         * @link https://php.net/manual/en/splsubject.detach.php
         * @param SplObserver $observer <p>
     * The <b>SplObserver</b> to detach.
         * </p>
         * @return void 
         * @since 5.1.0
         */
        public function detach (SplObserver $observer);

        /**
         * Notify an observer
         * @link https://php.net/manual/en/splsubject.notify.php
         * @return void 
         * @since 5.1.0
         */
        public function notify ();

}
/**
 * The <b>SplObserver</b> interface is used alongside
 * <b>SplSubject</b> to implement the Observer Design Pattern.
 * @link https://php.net/manual/en/class.splobserver.php
 */
interface SplObserver  {

        /**
         * Receive update from subject
         * @link https://php.net/manual/en/splobserver.update.php
         * @param SplSubject $subject <p>
     * The <b>SplSubject</b> notifying the observer of an update.
         * </p>
         * @return void 
         * @since 5.1.0
         */
        public function update (SplSubject $subject);

}
<?php

use SplObjectStorage;
use SplObserver;
use SplSubject;

/**
 * Class WeatherData
 */
class WeatherData implements SplSubject
{
    /**
     * @var SplObjectStorage
     */
    private SplObjectStorage $observers;
    /**
     * @var float
     */
    private float $temperature;
    /**
     * @var float
     */
    private float $humidity;
    /**
     * @var float
     */
    private float $pressure;

    /**
     * WeatherData constructor.
     */
    public function __construct()
    {
        $this->observers = new SplObjectStorage();
    }

    /**
     * @param SplObserver $observer
     */
    public function attach(SplObserver $observer): void
    {
        $this->observers->attach($observer);
    }

    /**
     * @param SplObserver $observer
     */
    public function detach(SplObserver $observer): void
    {
        $this->observers->detach($observer);
    }

    /**
     * SplObjectStorageに登録されたオブザーバを取り出して通知する
     */
    public function notify(): void
    {
        $observers = $this->observers;
        $observers->rewind();
        while ($observers->valid()) {
            $observers->current()->update($this);
            $observers->next();
        }
    }

    /**
     * 気候情報をセットする
     * @param float $temperature
     * @param float $humidity
     * @param float $pressure
     */
    public function setMeasurements(float $temperature, float $humidity, float $pressure): void
    {
        $this->temperature = $temperature;
        $this->humidity = $humidity;
        $this->pressure = $pressure;
        $this->measurementsChanged();
    }

    /**
     * 更新された設定値が設定されたら通知を送る
     */
    private function measurementsChanged(): void
    {
        $this->notify();
    }

    /**
     * @return float
     */
    public function getTemperature(): float
    {
        return $this->temperature;
    }

    /**
     * @return float
     */
    public function getHumidity(): float
    {
        return $this->humidity;
    }
}
<?php

use SplObserver;
use SplSubject;

class CurrentConditionDisplay implements SplObserver
{
    private float $temperature;
    private float $humidity;
    private SplSubject $weatherData;

    /**
     * CurrentConditionDisplay constructor.
     * @param SplSubject $weatherData
     */
    public function __construct(SplSubject $weatherData)
    {
        $this->weatherData = $weatherData;
        $weatherData->attach($this);
    }

    public function display(): void
    {
        echo "現在の気象状況:温度{$this->temperature}度 湿度{$this->humidity}%\n";
    }

    public function update(SplSubject $subject): void
    {
        if ($subject instanceof WeatherData) {
            $this->temperature = $subject->getTemperature();
            $this->humidity = $subject->getHumidity();
            $this->display();
        }
    }
}
<?php

$weatherData = new WeatherData();
$currentDisplay = new CurrentConditionDisplay($weatherData);
$statisticsDisplay = new StatisticsDisplay($weatherData);
$weatherData->setMeasurements(27.0, 65, 30.4);
$weatherData->detach($currentDisplay);
$weatherData->notify();

github.com