Наблюдатель (шаблон проектирования)
Наблюдатель | |
---|---|
Observer | |
| |
Тип | поведенческий |
Назначение |
|
Описан в Design Patterns | Да |
Наблюдатель (англ. Observer) — поведенческий шаблон проектирования. Также известен как «подчинённые» (англ. Dependents). Реализует у класса механизм, который позволяет объекту этого класса получать оповещения об изменении состояния других объектов и тем самым наблюдать за ними[2].
Классы, на события которых другие классы подписываются, называются субъектами (Subjects), а подписывающиеся классы называются наблюдателями (англ. Observers)[3].
Похожие шаблоны: «издатель — подписчик», «посредник», «одиночка».
Назначение
[править | править код]Определяет зависимость типа один ко многим между объектами таким образом, что при изменении состояния одного объекта все зависящие от него оповещаются об этом событии.
Реализация
[править | править код]При реализации шаблона «наблюдатель» обычно используются следующие классы:
- Observable — интерфейс, определяющий методы для добавления, удаления и оповещения наблюдателей;
- Observer — интерфейс, с помощью которого наблюдатель получает оповещение;
- ConcreteObservable — конкретный класс, который реализует интерфейс Observable;
- ConcreteObserver — конкретный класс, который реализует интерфейс Observer.
Область применения
[править | править код]Шаблон «наблюдатель» применяется в тех случаях, когда система обладает следующими свойствами:
- существует как минимум один объект, рассылающий сообщения;
- имеется не менее одного получателя сообщений, причём их количество и состав могут изменяться во время работы приложения;
- позволяет избежать сильного зацепления взаимодействующих классов.
Данный шаблон часто применяют в ситуациях, в которых отправителя сообщений не интересует, что делают получатели с предоставленной им информацией.
Примеры
[править | править код]/**
* В PHP осуществляется встроенная поддержка этого шаблона через входящее в поставку
* расширение SPL (Standard PHP Library):
* SplObserver - интерфейс для Observer (наблюдателя),
* SplSubject - интерфейс Observable (наблюдаемого),
* SplObjectStorage - вспомогательный класс (обеспечивает улучшенное сохранение и удаление
* объектов, в частности, реализованы методы attach() и detach()).
*/
class Observable implements SplSubject
{
private $storage;
function __construct()
{
$this->storage = new SplObjectStorage();
}
function attach(SplObserver $observer)
{
$this->storage->attach($observer);
}
function detach(SplObserver $observer)
{
$this->storage->detach($observer);
}
function notify()
{
foreach($this->storage as $obj)
{
$obj->update($this);
}
}
}
class ConcreteObserver implements SplObserver
{
private $observable;
private $index;
function __construct(Observable $observable)
{
static $sindex=0;
$this->index=$sindex++;
$this->observable = $observable;
$observable->attach($this);
}
function update(SplSubject $subject)
{
if($subject === $this->observable)
{
echo "Send notify to ConcreteObserver [$this->index]\n";
}
}
}
$observable = new Observable();
new ConcreteObserver($observable);
new ConcreteObserver($observable);
new ConcreteObserver($observable);
$observable->notify();
interface Observer
{
function notify($obj);
}
class ExchangeRate
{
static private $instance = NULL;
private $observers = array();
private $exchange_rate;
private function __construct()
{}
private function __clone()
{}
static public function getInstance()
{
if(self::$instance == NULL)
{
self::$instance = new ExchangeRate();
}
return self::$instance;
}
public function getExchangeRate()
{
return $this->exchange_rate;
}
public function setExchangeRate($new_rate)
{
$this->exchange_rate = $new_rate;
$this->notifyObservers();
}
public function registerObserver(Observer $obj)
{
$this->observers[] = $obj;
}
function notifyObservers()
{
foreach($this->observers as $obj)
{
$obj->notify($this);
}
}
}
class ProductItem implements Observer
{
public function __construct()
{
ExchangeRate::getInstance()->registerObserver($this);
}
public function notify($obj)
{
if($obj instanceof ExchangeRate)
{
// Update exchange rate data
print "Received update!\n";
}
}
}
$product1 = new ProductItem();
$product2 = new ProductItem();
ExchangeRate::getInstance()->setExchangeRate(4.5);
C#
[править | править код]using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
namespace Observer
{
/// <summary>
/// Observer Pattern Judith Bishop Jan 2007
/// Updated by Kobel' Bohdan 2013
///
/// The Subject runs in a thread and changes its state
/// independently. At each change, it notifies its Observers.
/// </summary>
class Program
{
static void Main(string[] args)
{
Subject subject = new Subject();
Observer observer = new Observer(subject,"Center","\t\t");
Observer observer2 = new Observer(subject,"Right","\t\t\t\t");
subject.Go();
// Wait for user
Console.Read();
}
}
class Simulator : IEnumerable
{
string [] moves = {"5","3","1","6","7"};
public IEnumerator GetEnumerator()
{
foreach (string element in moves)
yield return element;
}
}
interface ISubject
{
void AddObserver(IObserver observer);
void RemoveObserver(IObserver observer);
void NotifyObservers(string s);
}
class Subject : ISubject
{
public string SubjectState { get; set; }
public List<IObserver> Observers { get; private set; }
private Simulator simulator;
private const int speed = 200;
public Subject()
{
Observers = new List<IObserver>();
simulator = new Simulator();
}
public void AddObserver(IObserver observer)
{
Observers.Add(observer);
}
public void RemoveObserver(IObserver observer)
{
Observers.Remove(observer);
}
public void NotifyObservers(string s)
{
foreach (var observer in Observers)
{
observer.Update(s);
}
}
public void Go()
{
new Thread(new ThreadStart(Run)).Start( );
}
void Run ()
{
foreach (string s in simulator)
{
Console.WriteLine("Subject: " + s);
SubjectState = s;
NotifyObservers(s);
Thread.Sleep(speed); // milliseconds
}
}
}
interface IObserver
{
void Update(string state);
}
class Observer : IObserver
{
string name;
ISubject subject;
string state;
string gap;
public Observer(ISubject subject, string name, string gap)
{
this.subject = subject;
this.name = name;
this.gap = gap;
subject.AddObserver(this);
}
public void Update(string subjectState)
{
state = subjectState;
Console.WriteLine(gap + name + ": " + state);
}
}
}
// В примере описывается получение данных от метеорологической станции (класс WeatherData, рассылатель событий) и
//использование их для вывода на экран (класс CurrentConditionsDisplay, слушатель событий).
//Слушатель регистрируется у наблюдателя с помощью метода registerObserver (при этом слушатель заносится в список observers).
//Регистрация происходит в момент создания объекта currentDisplay, т.к. метод registerObserver применяется в конструкторе.
//При изменении погодных данных вызывается метод notifyObservers, который в свою очередь вызывает метод update
//у всех слушателей, передавая им обновлённые данные.
import java.util.LinkedList;
import java.util.List;
public class WeatherStation {
public static void main(String[] args) {
WeatherData weatherData = new WeatherData();
Observer currentDisplay = new CurrentConditionsDisplay (weatherData);
weatherData.setMeasurements(29f, 65f, 745);
weatherData.setMeasurements(39f, 70f, 760);
weatherData.setMeasurements(42f, 72f, 763);
}
}
interface Observer {
void update (float temperature, float humidity, int pressure);
}
interface Observable {
void registerObserver(Observer o);
void removeObserver(Observer o);
void notifyObservers();
}
class WeatherData implements Observable {
private List<Observer> observers;
private float temperature;
private float humidity;
private int pressure;
public WeatherData() {
observers = new LinkedList<>();
}
@Override
public void registerObserver(Observer o) {
observers.add(o);
}
@Override
public void removeObserver(Observer o) {
observers.remove(o);
}
@Override
public void notifyObservers() {
for (Observer observer : observers)
observer.update(temperature, humidity, pressure);
}
public void setMeasurements(float temperature, float humidity, int pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
notifyObservers();
}
}
class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;
private int pressure;
public CurrentConditionsDisplay(Observable weatherData) {
weatherData.registerObserver(this);
}
@Override
public void update(float temperature, float humidity, int pressure) {
this.temperature = temperature;
this.humidity = humidity;
this.pressure = pressure;
display();
}
public void display() {
System.out.printf("Сейчас значения:%.1f градусов цельсия и %.1f %% влажности. Давление %d мм рт. ст.\n", temperature, humidity, pressure);
}
}
#include <iostream>
#include <string>
#include <list>
using namespace std;
class SupervisedString;
class IObserver
{
public:
virtual void handleEvent(const SupervisedString&) = 0;
};
class SupervisedString // Observable class
{
string _str;
list<IObserver*> _observers;
void _Notify()
{
for(auto& observer: _observers)
{
observer->handleEvent(*this);
}
}
public:
void add(IObserver& ref)
{
_observers.push_back(&ref);
}
void remove(IObserver& ref)
{
_observers.remove(&ref);
}
const string& get() const
{
return _str;
}
void reset(string str)
{
_str = str;
_Notify();
}
};
class Reflector: public IObserver // Prints the observed string into cout
{
public:
virtual void handleEvent(const SupervisedString& ref)
{
cout << ref.get() << endl;
}
};
class Counter: public IObserver // Prints the length of observed string into cout
{
public:
virtual void handleEvent(const SupervisedString& ref)
{
cout << "length = " << ref.get().length() << endl;
}
};
int main()
{
SupervisedString str;
Reflector refl;
Counter cnt;
str.add(refl);
str.reset("Hello, World!");
cout << endl;
str.remove(refl);
str.add(cnt);
str.reset("World, Hello!");
cout << endl;
return 0;
}
//файл IObserver.as
package
{
public interface IObserver
{
function notify(obj:Object):void;
}
}
//файл ExchangeRate.as
package
{
public class ExchangeRate
{
private static var _instance:ExchangeRate = null;
private var observers:Array = [];
private var _exchangeRate:Object;
public function ExchangeRate()
{
if (_instance == null) throw new Error('Model Singleton!');
}
public static function getInstance():ExchangeRate
{
if (_instance == null) _instance = new ExchangeRate();
return _instance;
}
public function get exchangeRate():Object
{
return _exchangeRate;
}
public function set exchangeRate(value:Object):void
{
_exchangeRate = value;
this.notifyObservers();
}
public function registerObserver(value:IObserver):void
{
this.observers.push(value);
}
private function notifyObservers():void
{
for each (var observer:IObserver in this.observers)
{
observer.notify(this);
}
}
}
}
//файл ProductItem.as
package
{
public class ProductItem implements IObserver
{
public function ProductItem()
{
ExchangeRate.getInstance().registerObserver(this);
}
public function notify(value:Object):void
{
if (value is ExchangeRate)
{
var exchange:ExchangeRate = value as ExchangeRate;
trace(exchange.exchangeRate);
}
}
}
}
//файл Main.as
package
{
import flash.display.Sprite;
public class Main extends Sprite
{
public function Main():void
{
var item1:ProductItem = new ProductItem();
var item2:ProductItem = new ProductItem();
ExchangeRate.getInstance().exchangeRate = 3.5;
}
}
}
Imports System.Collections
Imports System.Threading
Namespace Observer
''' <summary>
''' Observer Pattern Judith Bishop Jan 2007
'''
''' The Subject runs in a thread and changes its state
''' independently. At each change, it notifies its Observers.
''' </summary>
Class Program
Shared Sub Main()
Dim subject As New Subject()
Dim Observer As New Observer(subject, "Center", vbTab & vbTab)
Dim observer2 As New Observer(subject, "Right", vbTab & vbTab & vbTab & vbTab)
subject.Go()
' Wait for user
Console.Read()
End Sub
End Class
Class Simulator
Implements IEnumerable
Private moves As String() = {"5", "3", "1", "6", "7"}
Public Function GetEnumerator() As IEnumerator Implements IEnumerable.GetEnumerator
Return moves.GetEnumerator ' // Yield
End Function
End Class
Class Subject
Public Delegate Sub Callback(ByVal s As String)
Public Event Notify As Callback
Private simulator As New Simulator()
Private m_SubjectState As String
Private Const speed As Integer = 200
Public Property SubjectState() As String
Get
Return m_SubjectState
End Get
Set(ByVal value As String)
m_SubjectState = value
End Set
End Property
Public Sub Go()
Call (New Thread(New ThreadStart(AddressOf Run))).Start()
End Sub
Private Sub Run()
For Each s As String In simulator
Console.WriteLine("Subject: " & s)
SubjectState = s
RaiseEvent Notify(s)
' milliseconds
Thread.Sleep(speed)
Next
End Sub
End Class
Interface IObserver
Sub Update(ByVal state As String)
End Interface
Class Observer
Implements IObserver
Private name As String
Private subject As Subject
Private state As String
Private gap As String
Public Sub New(ByVal subject As Subject, ByVal name As String, ByVal gap As String)
Me.subject = subject
Me.name = name
Me.gap = gap
AddHandler subject.Notify, AddressOf Update
End Sub
Public Sub Update(ByVal subjectState As String) Implements IObserver.Update
state = subjectState
Console.WriteLine(gap & name & ": " & state)
End Sub
End Class
End Namespace
from abc import ABCMeta, abstractmethod
class Observer(metaclass=ABCMeta):
"""
Абстрактный наблюдатель
"""
@abstractmethod
def update(self, message: str) -> None:
"""
Получение нового сообщения
"""
pass
class Observable(metaclass=ABCMeta):
"""
Абстрактный наблюдаемый
"""
def __init__(self) -> None:
"""
Constructor.
"""
self.observers = [] # инициализация списка наблюдателей
def register(self, observer: Observer) -> None:
"""
Регистрация нового наблюдателя на подписку
"""
self.observers.append(observer)
def notify_observers(self, message: str) -> None:
"""
Передача сообщения всем наблюдателям, подписанным на события
данного объекта наблюдаемого класса
"""
for observer in self.observers:
observer.update(message)
class Newspaper(Observable):
"""
Газета, за новостями в которой следят тысячи людей
"""
def add_news(self, news: str) -> None:
"""
Выпуск очередной новости
"""
self.notify_observers(news)
class Citizen(Observer):
"""
Обычный гражданин, который любит читнуть с утра любимую газетку
"""
def __init__(self, name: str) -> None:
"""
Constructor.
:param name: имя гражданина, чтоб не спутать его с кем-то другим
"""
self.name = name
def update(self, message: str) -> None:
"""
Получение очередной новости
"""
print(f'{self.name} узнал следующее: {message}')
if __name__ == '__main__':
newspaper = Newspaper() # создаем небольшую газету
newspaper.register(Citizen('Иван')) # добавляем двух человек, которые
newspaper.register(Citizen('Василий')) # ... ее регулярно выписывают
# ... и вбрасываем очередную газетную утку
newspaper.add_news('Наблюдатель - поведенческий шаблон проектирования')
'''
Иван узнал следующее: Наблюдатель - поведенческий шаблон проектирования
Василий узнал следующее: Наблюдатель - поведенческий шаблон проектирования
'''
program observer;
/// Observer Pattern Judith Bishop Jan 2007
/// Ported to Pascal by Dmitry Boyarintsev, May 2018
///
/// The Subject runs in a thread and changes its state
/// independently. At each change, it notifies its Observers.
{$ifdef fpc}{$mode delphi}{$H+}{$endif}
uses SysUtils, Classes;
type
TBaseObserver = class(TObject)
procedure Update(const astate: string); virtual; abstract;
end;
TBaseSubject = class(TObject)
procedure AddObserver(aobserver: TBaseObserver); virtual; abstract;
procedure RemoveObserver(aobserver: TBaseObserver); virtual; abstract;
procedure NotifyObservers(const s: string); virtual; abstract;
end;
type
{ TSubject }
TSubject = class(TBaseSubject)
private
fObservers : TList;
fSimulator : TStringList;
speed : Integer;
protected
procedure Run;
public
constructor Create;
destructor Destroy; override;
procedure AddObserver(aobserver: TBaseObserver); override;
procedure RemoveObserver(aobserver: TBaseObserver); override;
procedure NotifyObservers(const astate: string); override;
procedure Go;
end;
TObserver = class (TBaseObserver)
private
fname : string;
fsubject : TBaseSubject;
fstate : string;
fgap : string;
public
constructor Create(asubject: TBaseSubject; const aname, agap: string);
procedure Update(const astate: string); override;
end;
{ TSubject }
procedure TSubject.Run;
var
i : integer;
s : string;
begin
for i:=0 to fSimulator.Count-1 do begin
s := fSimulator[i];
Writeln('Subject: ',s);
NotifyObservers(s);
Sleep(speed); // milliseconds
end;
end;
constructor TSubject.Create;
begin
inherited Create;
fObservers := TList.Create;
speed := 200;
fSimulator := TStringList.Create;
fSimulator.AddStrings(['5','3','1','6','7']);
end;
destructor TSubject.Destroy;
begin
fObservers.Free;
fSimulator.Free;
end;
procedure TSubject.AddObserver(aobserver: TBaseObserver);
begin
fObservers.Add(aobserver);
end;
procedure TSubject.RemoveObserver(aobserver: TBaseObserver);
begin
fObservers.Remove(aobserver);
end;
procedure TSubject.NotifyObservers(const astate: string);
var
i : integer;
begin
for i:=0 to fObservers.Count-1 do
TBaseObserver(fObservers[i]).Update(astate);
end;
type
{ TMethodThread }
TMethodThread = class(TThread)
protected
fMethod: TThreadMethod;
procedure Execute; override;
public
constructor Create(AMethod: TThreadMethod);
end;
{ TMethodThread }
constructor TMethodThread.Create(AMethod: TThreadMethod);
begin
fMethod := AMethod;
FreeOnTerminate := True;
inherited Create(false);
end;
procedure TMethodThread.Execute;
begin
if Assigned(fMethod) then fMethod();
end;
procedure TSubject.Go;
begin
TMethodThread.Create(Self.Run);
end;
constructor TObserver.Create(asubject: TBaseSubject; const aname, agap: string);
begin
inherited Create;
fsubject := asubject;
fname := aname;
fgap := agap;
if Assigned(fsubject) then fsubject.AddObserver(self);
end;
procedure TObserver.Update(const astate: string);
begin
fstate := astate;
writeln(fgap, fname, ': ', astate);
end;
/// Main Program
var
subject : TSubject;
observer : TObserver;
observer2 : TObserver;
begin
subject := TSubject.Create;
observer := TObserver.Create(subject, 'Center', #9#9);
observer2 := TObserver.Create(subject,'Right',#9#9#9#9);
try
subject.Go();
// Wait for user
readln;
finally
observer.Free;
observer2.Free;
subject.Free;
end;
end.
module Observable
def initialize
@observers = []
end
def add_observer(observer)
@observers << observer unless @observers.include?(observer)
end
def delete_observer(observer)
@observers.delete(observer)
end
def notify_observers
@observers.each {|x| x.update(self)}
end
end
class Employee
include Observable
attr_reader :name
attr_accessor :title, :salary
def initialize(name, title, salary)
super()
@name = name
@title = title
@salary = salary
end
end
class BaseObserver
def update
raise 'Must be implement "update" function'
end
end
class Payroll < BaseObserver
def update(employee )
p("Cut a new check for #{employee.name}!")
p("His salary is now #{employee.salary}!")
end
end
class TaxMan < BaseObserver
def update(employee)
p("Send #{employee.name} a new tax bill!")
end
end
mike = Employee.new('Mike', 'project manger', 25000)
mike.add_observer(Payroll.new)
mike.add_observer(TaxMan.new)
mike.salary = 35000
mike.title = 'senior project manger'
mike.notify_observers
=begin
Результат
"Cut a new check for Mike!"
"His salary is now 35000!"
"Send Mike a new tax bill!"
=end
/// В примере описывается получение данных от метеорологической станции (структура WeatherData, рассылатель событий) и
/// использование их для вывода на экран (структура CurrentConditionsDisplay, слушатель событий).
/// Слушатель регистрируется у наблюдателя с помощью метода register_observer, который принимает замыкание и заносит его
/// в список observers. При изменении погодных данных вызывается метод notify_observers, который выполняет замыкания
/// всех слушателей, передавая им обновлённые данные.
use std::rc::Rc;
use std::cell::RefCell;
type ObserverFn = Box<dyn Fn(f32, f32, i32)>;
trait Observable {
fn register_observer(&mut self, o: ObserverFn) -> usize;
fn remove_observer(&mut self, idx: usize);
fn notify_observers(&mut self);
}
#[derive(Default)]
struct WeatherData {
observers: Vec<ObserverFn>,
temperature: f32,
humidity: f32,
pressure: i32,
}
impl WeatherData {
fn set_measurements(&mut self, temperature: f32, humidity: f32, pressure: i32) {
self.temperature = temperature;
self.humidity = humidity;
self.pressure = pressure;
self.notify_observers();
}
}
impl Observable for WeatherData {
fn register_observer(&mut self, o: ObserverFn) -> usize {
self.observers.push(o);
self.observers.len() - 1
}
fn remove_observer(&mut self, idx: usize) {
self.observers.remove(idx);
}
fn notify_observers(&mut self) {
for observer in self.observers.iter() {
(*observer)(self.temperature, self.humidity, self.pressure);
}
}
}
#[derive(Default)]
struct CurrentConditionsDisplay {
temperature: f32,
humidity: f32,
pressure: i32,
}
impl CurrentConditionsDisplay {
fn display(&self) {
println!("Сейчас значения: {:.1} градусов цельсия и {:.1} % влажности. Давление {} мм рт. ст.",
self.temperature, self.humidity, self.pressure);
}
fn update(&mut self, temperature: f32, humidity: f32, pressure: i32) {
self.temperature = temperature;
self.humidity = humidity;
self.pressure = pressure;
self.display();
}
}
fn main() {
let mut weather_data = WeatherData::default();
let current_display = Rc::new(RefCell::new(CurrentConditionsDisplay::default()));
let observer = current_display.clone();
weather_data.register_observer(Box::new(move |t, h, p| observer.borrow_mut().update(t, h, p)));
weather_data.set_measurements(29.0, 65.0, 745);
weather_data.set_measurements(39.0, 70.0, 760);
weather_data.set_measurements(42.0, 72.0, 763);
}
# Пример полностью идентичный тому, который выше на Python
Observer := Object clone
Observable := List clone do(
register := getSlot("push")
notify := method(message, self foreach(observer, observer update(message)))
)
Newspaper := Observable clone do( addNews := method(news, notify(news)))
Citizen := Observer clone do(
create := method(name, self clone lexicalDo(name := name))
update := method(message, writeln( name .. " узнал следующее: " .. message))
)
newspaper := Newspaper clone
newspaper do(
register(Citizen create("Иван"))
register(Citizen create("Василий"))
addNews("Наблюдатель - поведенческий шаблон проектирования")
)
#>>>> Иван узнал следующее: Наблюдатель - поведенческий шаблон проектирования
#>>>> Василий узнал следующее: Наблюдатель - поведенческий шаблон проектирования
class Observable {
constructor() {
this.listeners = {};
}
// Подписаться.
on(e, callback) {
if (this.listeners[e] == undefined) {
this.listeners[e] = {};
this.listeners[e].eventProperty = {};
this.listeners[e].eventProperty.isOnOnce = false;
this.listeners[e].data = [];
}
this.listeners[e].data.push(callback);
}
// Подписаться единожды.
onOnce(e, callback) {
this.on(e, callback);
this.listeners[e].eventProperty.isOnOnce = true;
}
// Отписаться.
off(e, callback) {
this.listeners[e].data = this.listeners[e].data.
filter(function (listener) { return listener !== callback; });
}
// Разослать сообщение подписчикам.
emit(e, data) {
if (this.listeners[e] == undefined || this.listeners[e].data == undefined) {
return;
}
let itObj = this;
this.listeners[e].data.forEach(listener => {
if(itObj.listeners[e].eventProperty.isOnOnce) {
itObj.off(e, itObj.listeners[e].data[0]);
}
listener(data);
});
}
}
Дополнительная информация
[править | править код]В платформе .NET Framework 4.0 шаблон разработки наблюдателя применяется путём реализации универсальных интерфейсов System.IObservable<T>
и System.IObserver<T>
[2].
Литература
[править | править код]- Эрих Гамма, Ричард Хелм, Ральф Джонсон, Джон Влиссидес. Приемы объектно-ориентированного проектирования. Паттерны проектирования = Design Patterns. Elements of Reusable Object-Oriented Software. — СПб.: Питер, 2009. — 366 с. — ISBN 978-5-469-01136-1.
- Эрик Фримен, Элизабет Фримен. Паттерны проектирования = Head First Design Patterns. — СПб.: Питер, 2011. — 656 с. — ISBN 978-5-459-00435-9.
Примечания
[править | править код]- ↑ Паттерн Observer . Дата обращения: 13 июня 2013. Архивировано 13 июня 2013 года.
- ↑ 1 2 Шаблон разработки Observer . Дата обращения: 13 июня 2013. Архивировано 13 июня 2013 года.
- ↑ Паттерн наблюдатель (Observer) . Дата обращения: 4 ноября 2019. Архивировано 4 ноября 2019 года.