Паттерн Наблюдатель (Observer) на реальном примере

Часто в вашем приложении есть данные, которые меняются со временем. Например, предположим, что есть данные интерфейса пользователя, которые необходимы для их отображения и последующего обновления при их изменении. Как вы бы справились с этим? Одним из решений может быть передача недавно обновленных данных в метод интерфейса. Проблема с этим подходом — не забывать, что каждый раз данные обновляются. Что делать, если неясно, как часто данные будут обновляться и будут ли, а вы хотите, чтобы интерфейс обновлялся автоматически.

Паттерн Наблюдатель решает эту проблему, используя два интерфейса: Observer и Observable. Исходя из названия предполагается, что Observer «наблюдает» за Observable «наблюдаемым», чтобы увидеть, изменяется ли он. Сохраняя тему человеческих чувств, Наблюдателя иногда называют Слушателем, но мы будем придерживаться прежнего названия. В своей самой базовой реализации Observable может добавлять наблюдателей. Наблюдаемый тогда отвечает для уведомления их, если что-либо в своем состоянии изменилось, а Наблюдатель отвечает за реагирование на изменения.

interface Observer {
 public function update(Observable $subject);
}

abstract class Widget implements Observer {
 protected $internalData = array();
 abstract public function draw();
 public function update(Observable $subject) {
 $this->internalData = $subject->getData();
 }
}

class BasicWidget extends Widget {

 function __construct() {

 }

 public function draw() {
 $html = "<table border=1 width=130>";
 $html .= "<tr> <td colspan=3 bgcolor=#cccccc>
 <b> Instrument Info </b> </td > </tr>";
 $numRecords = count($this->internalData[0]);
 for($i = 0; $i < $numRecords; $i++) {
 $instms = $this->internalData[0];
 $prices = $this->internalData[1];
 $years = $this->internalData[2];
 $html .= "<tr> <td> $instms[$i] </td> <td>$prices[$i] </td>
 <td> $years[$i] </td> </tr>";
 }
 $html .= "</table> <br>";
 echo $html;
 }
}

class FancyWidget extends Widget {

 function __construct() {

 }

public function draw() {
$html = "<table border=0 cellpadding=5 width=270>
 <tr> <td colspan=3 bgcolor=#cccccc>
 <b> <span> Our Latest Prices <span> <b>
 </td> </tr>
 <tr> <td> <b> instrument </b> </td>
 <td> <b> price </b> </td> <td> <b> date issued </b>
 </td> </tr>";

 $numRecords = count($this->internalData[0]);

 for($i = 0; $i < $numRecords; $i++) {

 $instms = $this->internalData[0];
 $prices = $this->internalData[1];
 $years = $this->internalData[2];
 $html .= "<tr> <td> $instms[$i] </td> <td>
 $prices[$i] </td> <td> $years[$i]
 </td> </tr>";
 }

 $html .= "</table> <br>";
 echo $html;

 }}

Объект DataSource инкапсулирует название, цену и дату выпуска группы музыкальных инструментов. Это же объект Observable. Ключевыми методами в Observer являются addObserver() и
notifyObservers(). Любого наблюдателя (в данном случае любой виджет), которому необходимо «наблюдать» за источником данных можно добавить с помощью addObserver().

abstract class Observable {
 private $observers = array();

 public function addObserver(Observer $observer) {
 array_push($this->observers, $observer);
 }

 public function notifyObservers() {
 for ($i = 0; $i < count($this->observers); $i++) {
 $widget = $this->observers[$i];
 $widget->update($this);
 }
 }

}

class DataSource extends Observable {

 private $names;
 private $prices;
 private $years;

 function __construct() {
 $this->names = array();
 $this->prices = array();
 $this->years = array();

 }

 public function addRecord($name, $price, $year) {
 array_push($this->names, $name);
 array_push($this->prices, $price);
 array_push($this->years, $year);
 $this->notifyObservers();

 }

 public function getData() {
 return array($this->names, $this->prices, $this->years);
 }
}

Метод addRecord() позволяет добавить новый инструмент во внутреннюю память объекта DataSource. Однако обратите внимание, что метод addRecord() выполняет еще одну вещь:

$this->notifyObservers();

Каждый раз, когда объект DataSource изменяет свои внутренние данные, он уведомляет всех своих наблюдателей. Наблюдатели
добавленны с использованием ранее упомянутого метода addObserver(). После добавления наблюдателя он сохраняется во внутренний массив наблюдателей. Когда вызывается notifyObservers(), метод проходит через массив $observers, вызывающий метод update() для каждого Observer, хранящегося в массиве. Единственный параметр метода обновления — это копия самого объекта Observable:

$widget->update($this); 

Это позволяет виджету (Наблюдателю) получать копию самого последнего состояния источника данных (Наблюдаемого). Обратите внимание, что виджет передается по значению — копии DataSource, а не фактического ссылка. Таким образом, внутренняя информация DataSource не передается.

Соединение Observer и Observable

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

$dat = new DataSource();
$widgetA = new BasicWidget();
$widgetB = new FancyWidget();

$dat->addObserver($widgetA);
$dat->addObserver($widgetB);

$dat->addRecord("drum", "$12.95", 1955);
$dat->addRecord("guitar", "$13.95", 2003);
$dat->addRecord("banjo", "$100.95", 1945);
$dat->addRecord("piano", "$120.95", 1999);

$widgetA->draw();
$widgetB->draw();

Все, что вам нужно сделать, это создать объекты DataSource и Widget. Затем вы можете добавить объекты Widget в качестве наблюдателей за источником данных. Теперь вы можете добавлять столько записей, сколько необходимо в DataSource объект, не заботясь об обновлении виджетов — все происходит автоматически. Наконец, когда вы вызываете draw() для виджетов, они отобразят себя с правильными данными.

Если вы разрабатывали настольное приложение, вы могли бы включить функцию redraw() в update() объекта Observer. Таким образом, вам не нужно будет явно вызывать draw(), а вместо этого
компонент автоматически перерисовывает себя в ответ на обновление источника данных. В чисто серверных веб-приложениях, нет возможности перерисовать компонент, пока вы не перезагрузите страницу.

Добавить комментарий