Блог о программировании

Шаблон Observer на PHP

 26 сентября 2016 г. 18:28

Observer(Наблюдатель) - является поведенческим шаблоном проектирования. Является достаточно популярным шаблоном проектирования, но, при этом, очень прост в реализации. Данный шаблон предполагает зависимость между объектами "один ко многим" так, что при изменении состояния одного объекта все зависящие от него объекты уведомляются и обновляются автоматически. Таким образом, в шаблоне наблюдаются две роли: субъект и слушатель. Шаблон "Наблюдатель"определяется следующими свойствами:

  • существует, как минимум, один субъект, рассылающий сообщения;
  • имеется не менее одного получателя сообщений, причём их количество и состав могут изменяться во время работы приложения;
  • нет надобности очень сильно связывать взаимодействующие объекты, что полезно для повторного использования.

Шаблон подходит для любого сценария, в котором требуется использование push-уведомлений. Очень часто его можно заметить в системах пользовательского интерфейса.


Аналогия

Самая банальная аналогия - подписка на рассылки какого-либо сайта. Когда на сайте появляется новый материал, то всем подписчикам рассылается e-mail-уведомление о появлении нового материала, а подписчики уже принимают решение просматривать новый материал или нет.


UML-диаграмма

Типовая UML-диаграмма выглядит следующим образом:

UML-диаграмма шаблона Observer

На ней присутствуют следующие сущности:

  • IObservable(наблюдаемый) — интерфейс, определяющий методы для добавления, удаления и оповещения наблюдателей;
  • IObserver(наблюдатель) — интерфейс, с помощью которого наблюдатель получает оповещение;
  • ConcreteObservable — конкретный класс, который реализует интерфейс IObservable;
  • ConcreteObserver — конкретный класс, который реализует интерфейс IObserver.

Код шаблона

Фактически, для реализации шаблона "Наблюдатель" необходимо описать два интерфейса с необходимыми методами: IObservable и IObserver. Код интерфейсов:

interface IObservable {
    /**
     * Добавление нового наблюдателя
     *
     * @param IObserver $instance
     * @return bool
     */
    public function attach(IObserver $instance);
    
    /**
     * Удаление имеющегося наблюдателя
     *
     * @param IObserver $instance     
     * @return bool    
     */    
    public function detach(IObserver $instance);    
     
    /**     
     * Оповещение всех наблюдателей, через вызов у него метода update     
     *     
     */    
    public function notify();
}

interface IObserver {    
    /**
     * Будет вызван у каждого наблюдателя в notify()    
     *     
     * @param IObservable $instance     
     * @return mixed     
     */    
    public function update(IObservable $instance);
}

Реализация методов, указанных в интерфейсах ложится на классы, где эти самые интерфейсы и будут использованы. Например, используя данный шаблон проектирования, мы будем следить за погодой. Допустим, у нас есть класс, который генерирует погоду и несколько слушателей, которые подписаны на обновление прогноза погоды. В коде это выглядит следующим образом:

class WeatherGenerator implements IObservable {
    /**    
     * Массив наблюдателей     
     *     
     * @var IObserver[]     
     */    
    private $observers = array();    
     
    /**    
     * Температура     
     *     
     * @var float    
     */    
    private $temperature;    
      
    /**    
     * Давление    
     *     
     * @var float     
     */    
    private $pressure;    
    
    public function attach(IObserver $instance) {        
        foreach ($this->observers as $observer) {            
            if ($instance === $observer) {                
                return false;            
            }        
        }        
        $this->observers[] = $instance;        
        return true;    
    }    
    
    public function detach(IObserver $instance) {        
        foreach ($this->observers as $key => $observer) {            
            if ($instance === $observer) {                
                unset($this->observers[$key]);                
                return true;            
            }        
        }        
        
        return false;    
    }    
    
    public function notify() {        
        foreach ($this->observers as $observer) {            
            $observer->update($this);        
        }    
    }    
    
    public function setParams($aTemperature, $aPressure) {        
        $this->temperature = $aTemperature;        
        $this->pressure = $aPressure;        
        $this->notify();    
    }    
    
    public function getTemperature() {        
        return $this->temperature;    
    }    
    
    public function getPressure() {        
        return $this->pressure;    
    }
}

class WeatherListener1 implements IObserver {    
    private $currentTemperature;    
    private $currentPressure;    
    
    public function update(IObservable $instance) {        
        $this->currentTemperature = $instance->getTemperature();        
        $this->currentPressure = $instance->getPressure();        
        $this->display();    
    }    
    
    public function display() {        
        echo '<b>WeatherListener1</b> Текущая температура: '.$this->currentTemperature.'; Текущее давление: '.$this->currentPressure.'<br/>';    
    }
}

class WeatherListener2 implements IObserver {    
    private $currentTemperature;    
    private $currentPressure;    
    
    public function update(IObservable $instance) {        
        $this->currentTemperature = $instance->getTemperature();        
        $this->currentPressure = $instance->getPressure();        
        $this->display();    
    }    
    
    public function display() {        
        echo '<b>WeatherListener2</b> Текущая температура: '.$this->currentTemperature.'; Текущее давление: '.$this->currentPressure.'<br/>';    
    }
}

Ну и добавим код приложения:

$wl1 = new WeatherListener1();
$wl2 = new WeatherListener2();
$weatherGenerator = new WeatherGenerator();
$weatherGenerator->attach($wl1);
$weatherGenerator->attach($wl2);
$weatherGenerator->setParams(14, 770);
$weatherGenerator->setParams(16, 750);
$weatherGenerator->detach($wl1);
$weatherGenerator->setParams(17, 745);

Результат работы скрипта:

WeatherListener1 Текущая температура: 14; Текущее давление: 770
WeatherListener2 Текущая температура: 14; Текущее давление: 770
WeatherListener1 Текущая температура: 16; Текущее давление: 750
WeatherListener2 Текущая температура: 16; Текущее давление: 750
WeatherListener2 Текущая температура: 17; Текущее давление: 745

Что в итоге? У нас есть два объекта классов, реализующих интерфейс слушателя(WeatherListener1, WeatherListener2) и один объект класса, генерирующего погоду, который также может оповещать об изменениях погоды(WeatherGenerator). Изначально оба слушателя оповещаются об изменениях погоды, но после второго изменения один из слушателей перестает оповещаться о погоде. Реализация шаблона "Наблюдатель" продемонстрирована в действии.


Как улучшить реализацию?

При анализе шаблона можно сделать вывод, что реализация интерфейса Observable будет всегда выглядеть одинаково. В таком случае целесообразно этот код вынести в отдельный класс и от него наследоваться. Такой подход имеет один небольшой минус - создается ненужная цепочка наследования. Но в php 5.4.0 появилось идеальное, более прогрессивное решение для таких случаев - трейты. Можно и нужно создать трейт, который будет реализовывать интерфейс Observable и впоследствии использовать этот трейт в классе, за которым нужно следить. Код трейта в таком случае будет таким:

trait TObservable {
    /**    
     * Массив наблюдателей     
     *     
     * @var TObserver[]     
     */    
    private $observers = array();    
    
    public function attach(IObserver $instance) {        
        foreach ($this->observers as $observer) {            
            if ($instance === $observer) {                
                return false;            
            }        
        }        
        $this->observers[] = $instance;        
        return true;    
    }    
    
    public function detach(IObserver $instance) {        
        foreach ($this->observers as $key => $observer) {            
            if ($instance === $observer) {                
                unset($this->observers[$key]);                
                return true;            
            }        
        }        
        return false;    
    }    
    
    public function notify() {        
        foreach ($this->observers as $observer) {            
            $observer->update($this);        
        }    
    }
}

А использовать его следует таким образом:

class WeatherGenerator implements IObservable {
    use TObservable;    
    ...
}

В таком случае, UML-диаграмма реализации будет следующей:

Модифицированная UML-диаграмма шаблона Observer

Поделиться статьей

Оставить комментарий