【PHPデザインパターン】18_Observer～状態変化を通知する

# 引用記事
この記事を書くきっかけになったブログです。

記事内の解説やソースコードは、こちらのブログと著者の公開リポジトリを参考にしています。

> [Do You PHP はてな〜[doyouphp][phpdp]PHPによるデザインパターン入門 - Observer～状態変化を通知する](http://d.hatena.ne.jp/shimooka/20141218/1418888729)

# 概要
- 「observer」は「観察者」「観測者」を意味する。
- オブジェクトの変化を他のオブジェクトに通知する。
- 観測対象のオブジェクトに変化があったとき、それを観測しているすべてのオブジェクトに通知を行う。
- オブジェクトどうしをゆるく結合したまま、協調動作をさせることを可能にする。

## 構成要素
### Subjectクラス
- 観測対象のクラス。
- 内部に観測クラスであるObserver型のオブジェクトを保持する。
- Observer型のオブジェクトを登録、削除するAPI、またObserver型のオブジェクトに通知をおこなうAPIが提供されている。

### Observerクラス
- 観測クラス。
- 通知を受け取るためのAPIを定義する。
- Listener、Handlerと呼ばれる場合もある。

### ConcreteSubjectクラス
- Subjectクラスのサブクラス。
- Observerクラスに影響する状態を保持し、その状態を取得するためのAPIを提供する。
- 保持した状態が変化したとき、Observer型のオブジェクトに通知を送る。

### ConcreteObserverクラス
- Observerクラスのサブクラス。
- 通知を受け取った場合の具体的な処理内容を記述する。

# 実演
## 処理の流れ
- カートに商品が追加されたらログを表示し、特定の商品がカートに入ったらプレゼントが付与される。
- ``Cartクラス``の状態が観測される。
- カートの内部状態を表示する``LoggingListenerクラス``と、特定の商品が入ったらプレゼントをカートに追加する``PresentListenerクラス``を実装する。
- カートに商品が追加されたら、Cartクラスを通じて各Listenerクラスへ通知する。
- 各Listenerクラスでカートの状態に応じた値をセット・出力する。

## ファイル構造
```sh
MyObserver
  ├── Cart.php
  ├── CartListener.php
  ├── LoggingListener.php
  ├── PresentListener.php
  └── my_client.php
```

## ソースコード
### Subjectクラス
兼ConcreteSubjectクラス。
#### Cart.php
```Cart.php
<?php
namespace DoYouPhp\PhpDesignPattern\Observer\MyObserver;


/**
 * Subjectクラス＋ConcreteSubjectクラスに相当する
 */
class Cart
{
    // 商品名をキー、数量を値として保持する
    private $items;
    // Listenerクラスのインスタンスを配列として保持する
    private $listeners;

    public function __construct()
    {
        $this->items = array();
        $this->listeners = array();
    }

    public function addItem($item_cd)
    {
        // 既にその商品がセットされていれば個数を加算、なければ1をセット
        $this->items[$item_cd] = (isset($this->items[$item_cd]) ? ++$this->items[$item_cd] : 1);

        // Listenerクラスへ通知する
        $this->notify();
    }

    public function removeItem($item_cd)
    {
        // 既にその商品がセットされていれば個数を減算、なければ0をセット
        $this->items[$item_cd] = (isset($this->items[$item_cd]) ? --$this->items[$item_cd] : 0);
        // 個数が0以下であれば、要素自体をを削除する
        if ($this->items[$item_cd] <= 0) {
            unset($this->items[$item_cd]);
        }

        // Listenerクラスへ通知する
        $this->notify();
    }

    // カートの商品を取得する
    public function getItems()
    {
        return $this->items;
    }

    // 指定した商品がカートに存在するか確認する
    public function hasItem($item_cd)
    {
        return array_key_exists($item_cd, $this->items);
    }

    // Listener型インスタンスを登録するメソッド
    // Listenerクラス名をキーにして、配列としてセットする
    public function addListener(CartListener $listener)
    {
        $this->listeners[get_class($listener)] = $listener;
    }

    // Listener型インスタンスを削除する
    public function removeListener(CartListner $listener)
    {
        unset($this->listeners[get_class($listener)]);
    }

    // Listener型インスタンスへ通知する
    public function notify()
    {
        foreach ($this->listeners as $listener) {
            $listener->update($this);
        }
    }

    // 現在のカートの状態を表示する
    public function show()
    {
        $line = str_repeat('-', 40).'<br>'."\n";

        echo $line;
        echo "商品名\t個数".'<br>'."\n";
        echo $line;
        foreach ($this->getItems() as $item_name => $quantity) {
            echo $item_name."\t".$quantity.'<br>'."\n";
        }

        echo $line;
    }
}
```

### Observerクラス
#### CartListener.php
```CartListener.php
<?php
namespace DoYouPhp\PhpDesignPattern\Observer\MyObserver;


/**
 * Observerクラスに相当する
 */
interface CartListener
{
    public function update(Cart $cart);
}
```

### ConcreteObserverクラス
#### PresentListener.php
```PresentListener.php
<?php
namespace DoYouPhp\PhpDesignPattern\Observer\MyObserver;


/**
 * ConcreteObserverクラスに相当する
 * Observerクラスのメソッドを実装する
 */
class PresentListener implements CartListener
{
    // 定数をセットする
    const PRESENT_TARGET_ITEM = 'クッキーセット';
    const PRESENT_ITEM = 'プレゼント';

    public function __construct()
    {
    }

    // クッキーセットをカートに入れた場合はプレゼントを付与する
    public function update(Cart $cart)
    {
        // クッキーセットがカートに存在する状態でプレゼントが付与されていない場合は、プレゼントを付与する
        if ($cart->hasItem(self::PRESENT_TARGET_ITEM) && !$cart->hasItem(self::PRESENT_ITEM)) {
            $cart->addItem(self::PRESENT_ITEM);
        }
        // クッキーセットがカートに存在しない状態でプレゼントが付与されている場合は、プレゼントを削除する
        if (!$cart->hasItem(self::PRESENT_TARGET_ITEM) && $cart->hasItem(self::PRESENT_ITEM)) {
            $cart->removeItem(self::PRESENT_ITEM);
        }
    }
}
```

#### LoggingListener.php
```LoggingListener.php
<?php
namespace DoYouPhp\PhpDesignPattern\Observer\MyObserver;


/**
 * ConcreteObserverクラスに相当する
 * Observerクラスのメソッドを実装する
 */
class LoggingListener implements CartListener
{
    public function __construct()
    {
    }

    // 現在のカートの状態を配列形式で表示する
    // addItemメソッドやremoveItemメソッドが実行される度に表示される
    public function update(Cart $cart)
    {
        echo var_export($cart->getItems(), true).'<br>'."\n";
    }
}
```

### Clientクラス
#### my_client.php
```my_client.php
<?php
namespace DoYouPhp\PhpDesignPattern\Observer\MyObserver;

require dirname(dirname(__DIR__)).'/vendor/autoload.php';

// カートを作成する
$cart = new Cart();

// Listenerを登録する
$cart->addListener(new PresentListener());
$cart->addListener(new LoggingListener());

// プレゼント対象外の商品を追加する
$cart->addItem("Tシャツ");
$cart->addItem("ぬいぐるみ");
$cart->addItem("ぬいぐるみ");
$cart->show();

// プレゼント対象の商品を追加する
$cart->addItem("クッキーセット");
$cart->show();

// プレゼント対象の商品を削除する
$cart->removeItem("クッキーセット");
$cart->show();
```