「HEAD-FIRST」之观察者模式

1,042 阅读2分钟

这是一个设计模式系列,本书所有案例均来自「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,进行有效的更新.