「HEAD-FIRST」之觀察者模式

godruoyi發表於2019-03-04

這是一個設計模式系列,本書所有案例均來自「Head-First設計模式(中文版)」, 所有系列文章將同步到 Github地址, 歡迎大家 watch, star

觀察者模式

定義了物件之間的一對多依賴,當一個物件改變狀態時,它的所有依賴者都將會收到通知並自動更新.

觀察者模式形容圖

godruoyi-觀察者模式

設計謎題

有一個氣象觀察站,我們希望建立一個應用,有三種佈告板(用於顯示不同的氣象資料),當氣象站獲取到最新的測量資料時,我們希望三種佈告板能實時更新.

類圖設計

godruoyi-觀察者模式

其中 WeatherData用於獲取氣象站最新測量資料(三個get方法),當資料更新時,會呼叫onChanged方法(不要管為什麼,這是氣象站內部邏輯).

程式碼實現

主題介面

interface Sublect
{
    public function registerObserver(Observer $observer);

    public function removeObserver();

    public function nitifyObservers();
}
複製程式碼

主題物件 WeatherData

class WeatherData implements Sublect
{
    protected $observers = [];

    protected $pressure, $temperature, $humidity;

    public function registerObserver(Observer $observer)
    {
        if (array_search($observer, $this->observers) === false) {
            $this->observers[] = $observer;
        }
    }

    public function removeObserver()
    {
        if (($index = array_search($observer, $this->observers)) !== false) {
            unset($this->observers[$index]);
        }
    }

    public function nitifyObservers()
    {
        foreach ($this->observers as $observer) {
            $observer->update($this->getPressure(), $this->getTemperature(), $this->getHumidity());
        }
    }

    public function onChanged()
    {
        $this->nitifyObservers();
    }

    //獲取最新氣壓
    public function getPressure()
    {
        return $this->pressure;
    }

    //獲取最新溫度
    public function getTemperature()
    {
        return $this->temperature;
    }

    //獲取最新溼度
    public function getHumidity()
    {
        return $this->humidity;
    }

    //測試
    public function youNeedChanged()
    {
        $this->pressure = mt_rand(1, 99);
        $this->temperature = mt_rand(1, 99);
        $this->humidity = mt_rand(1, 99);

        $this->onChanged();
    }
}
複製程式碼

觀察者介面

interface Observer
{
    //氣壓/溫度/溼度
    public function update($pressure, $temperature, $humidity);
}
複製程式碼

顯示皮膚介面

interface DisplayElement
{
    public function display();
}
複製程式碼

觀察者物件集

class CurrentConditionsDisplay implements Observer, DisplayElement
{
    protected $subject;

    protected $pressure, $temperature, $humidity;

    //這裡為什麼會保留 Subject 介面的引用是為了方便的 remove 及 registe
    public function __construct(Sublect $subject)
    {
        $this->subject = $subject;
        $this->subject->registerObserver($this);
    }

    public function update($pressure, $temperature, $humidity)
    {
        $this->pressure = $pressure;
        $this->temperature = $temperature;
        $this->humidity = $humidity;

        $this->display();
    }

    public function display()
    {
        echo "Current pressure: {$this->pressure}, Current temperature: {$this->temperature}";
    }
}

//其他兩種佈告板省略
複製程式碼

測試

$weatherData = new WeatherData();

$display = new CurrentConditionsDisplay($weatherData);//把當前布告欄註冊成為觀察者
//$other = new OthersDisplay($weatherData);//把當前布告欄註冊成為觀察者
//$other = new OtherDisplay($weatherData);//把當前布告欄註冊成為觀察者

$weatherData->youNeedChanged();//氣象站資料更新了會導致佈告板實時更新
//Current pressure: 33, Current temperature: 46
複製程式碼

另一種形式的觀察者模式

我們知道,觀察者總是被動的接受主題物件的推送,但有些場景下,我們希望觀察者能主動的去獲取資料;畢竟觀察者數量這麼多,主題物件不可能事先知道每個觀察者需要的狀態,並且也不會導致明明只需要一點點資料,卻被迫收到一堆.

我們來重寫設計上面的問題.

類圖基本保持不變,只是在WeatherData類新增了setChanged方法並改變了Observer介面update簽名.

重構後的主題介面

interface Sublect
{
    public function registerObserver(Observer $observer);
    public function removeObserver();
    public function nitifyObservers($args = null);
}

interface Observer
{
    public function update(Sublect $subject, $object = null);
}
複製程式碼

重構後的主題物件

class WeatherData implements Sublect
{
    protected $observers = [];

    protected $pressure, $temperature, $humidity, $changed;

    public function nitifyObservers($args = null)
    {
        if ($this->changed) {
            foreach ($this->observers as $observer) {
                $observer->update($this, $args);
            }
            $this->changed = false;
        }
    }

    public function onChanged()
    {
        $this->setChanged();

        $this->nitifyObservers([
            'pressure' => $this->pressure,
            'temperature' => $this->temperature,
            'humidity' => $this->humidity,
        ]);
    }

    public function setChanged()//新增方法
    {
        $this->changed = true;
    }

    //其他方法保持不變
}
複製程式碼

重構後的佈告板物件

class CurrentConditionsDisplay implements Observer, DisplayElement
{
    protected $subject;

    protected $pressure, $temperature, $humidity;

    //這裡為什麼會保留 Subject 介面的引用是為了方便的 remove 及 registe
    public function __construct(Sublect $subject)
    {
        $this->subject = $subject;
        $this->subject->registerObserver($this);
    }

    public function update(Sublect $subject, $object = null)
    {
        if ($subject instanceof Sublect) {
            //你可以用 拉取 的形式獲取最新資料
            $this->pressure = $subject->getPressure();
            $this->temperature = $subject->getTemperature();
            $this->humidity = $subject->getHumidity();

            //也可以從推送資料中獲取
            $this->pressure = $object['pressure'];
            $this->temperature = $object['temperature'];
            $this->humidity = $object['humidity'];
        }


        $this->display();
    }

    public function display()
    {
        echo "Current pressure: {$this->pressure}, Current temperature: {$this->temperature}";
    }
}
複製程式碼

為什麼要加一個 setChanged 方法

setChanged 讓你在更新觀察者時,有更多的彈性,能更適當的通知觀察者,比方說,如果沒有setCanged 方法,氣象站溫度變化十分之一度時,都會通知所有觀察者,你肯定不想讓這麼頻繁的更新吧.我們可以控制溫度變化達到一度時,呼叫 setChanged,進行有效的更新.

相關文章