Оригинальный текст — https://addyosmani.com/
Не забудьте про вторую часть.
Наблюдатель представляет собой шаблон проектирования, в котором объект (в данном паттерне именуются субъектом) хранит список объектов (наблюдателей), зависящих от его состояния. При любом изменении состояния субъекта происходит автоматическое уведомление наблюдателей. Когда с субъектом произошло что-то интересное, он проходит по списку наблюдателей и сообщает им об этом («что-то интересное» может содержать конкретные данные).
Если мы больше не хотим, чтобы какой-либо наблюдатель был привязан к изменениям субъекта, то мы можем удалить его списка наблюдателей.
Часто бывает полезно обращаться к исходным определениям паттернов (которые в свою очередь являются частью мира Language agnostic), чтобы получить более широкое представление о преимуществах и их использовании. Определение паттерна Observer в книге «GoF», Design Patterns: Elements of Reusable Object-Oriented Software:
«Интересующийся состоянием субъекта наблюдатель (наблюдателей может быть любое количество) подписываются к нему. Когда что-то меняется в нашем субъекте, наблюдателю отправляется уведомление, которое вызывает метод обновления у каждого «подписчика». Когда наблюдателя больше не интересует состояние субъекта, то он отписывается от него».
Давайте теперь определим основных участников работы нашего нового паттерна:
- Subject — он же субъект: хранит информацию о своих наблюдателях и позволяет их добавлять и удалять.
- Observer — он же наблюдатель: определяет интерфейс обновления для объектов, которые должны быть уведомлены об изменении состояния субъекта.
- ConcreteSubject — он же конкретный субъект: посылает уведомления наблюдателям об изменение состояния, сохраняет состояние ConcreteObservers
- ConcreteObserver — он же конкретный наблюдатель: хранит ссылку на ConcreteSubject, реализует интерфейс обновления для Observer, чтобы поддержать согласованность с субъектом.
Для начала давайте смоделируем список зависимых наблюдателей, которые могут иметь субъект:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
function ObserverList(){ this.observerList = []; } ObserverList.prototype.add = function( obj ){ return this.observerList.push( obj ); }; ObserverList.prototype.count = function(){ return this.observerList.length; }; ObserverList.prototype.get = function( index ){ if( index > -1 && index < this.observerList.length ){ return this.observerList[ index ]; } }; ObserverList.prototype.indexOf = function( obj, startIndex ){ var i = startIndex; while( i < this.observerList.length ){ if( this.observerList[i] === obj ){ return i; } i++; } return -1; }; ObserverList.prototype.removeAt = function( index ){ this.observerList.splice( index, 1 ); }; |
Затем смоделируем Субъект, а также возможность добавлять, удалять, уведомлять наблюдателей из списка наблюдателей.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function Subject(){ this.observers = new ObserverList(); } Subject.prototype.addObserver = function( observer ){ this.observers.add( observer ); }; Subject.prototype.removeObserver = function( observer ){ this.observers.removeAt( this.observers.indexOf( observer, 0 ) ); }; Subject.prototype.notify = function( context ){ var observerCount = this.observers.count(); for(var i=0; i < observerCount; i++){ this.observers.get(i).update( context ); } }; |
Затем мы определяем скелет для создания новых наблюдателей. В метод update
мы можем добавить необходимую функциональность нашего приложения.
1 2 3 4 5 6 |
// The Observer function Observer(){ this.update = function(){ // ... }; } |
Для большей наглядности мы создадим HTML разметку, которая будет содержать:
- кнопку добавления новых наблюдателей (в виде чекбоксов).
- контрольный чекбокс, который будет действовать как субъект, уведомляя другие флажки, что они должны быть в состоянии checked.
- контейнер, в который будут добавлять чекбоксы.
Затем мы определяем обработчики ConcreteSubject и ConcreteObserver для добавления новых наблюдателей на страницу и реализации интерфейса обновления. Ниже приведен код с инлайновыми комментариями.
HTML:
1 2 3 |
<button id="addNewObserver">Add New Observer checkbox</button> <input id="mainCheckbox" type="checkbox"/> <div id="observersContainer"></div> |
Наш скрипт:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
// Extend an object with an extension function extend( obj, extension ){ for ( var key in extension ){ obj[key] = extension[key]; } } // References to our DOM elements var controlCheckbox = document.getElementById( "mainCheckbox" ), addBtn = document.getElementById( "addNewObserver" ), container = document.getElementById( "observersContainer" ); // Concrete Subject // Extend the controlling checkbox with the Subject class extend( controlCheckbox, new Subject() ); // Clicking the checkbox will trigger notifications to its observers controlCheckbox.onclick = function(){ controlCheckbox.notify( controlCheckbox.checked ); }; addBtn.onclick = addNewObserver; // Concrete Observer function addNewObserver(){ // Create a new checkbox to be added var check = document.createElement( "input" ); check.type = "checkbox"; // Extend the checkbox with the Observer class extend( check, new Observer() ); // Override with custom update behaviour check.update = function( value ){ this.checked = value; }; // Add the new observer to our list of observers // for our main subject controlCheckbox.addObserver( check ); // Append the item to the container container.appendChild( check ); } |
В этом примере показано, как реализовать и использовать паттерн Observer, охватывающий концепции Subject, Observer, ConcreteSubject и ConcreteObserver.
Различия между паттернами Observer и Publish/Subscribe
Несмотря на то, что знать такой паттерн как Observer нужно каждому разработчику, в мире JavaScript мы можем обнаружить, что он обычно реализуется с использованием другой вариации, известной как паттерн Publish/Subscribe. Они очень похожи, но всё же между ними есть различия, которые стоит отметить.
Паттерн Observer требует, чтобы наблюдатель (или объект), желающий получать уведомления, должен быть подписан к объекту, запускающему событие, т.е. к субъекту.
Однако паттерн Publish/Subscribe использует topic/event channel (канал тема/событие) , который находится между объектами, желающими получать уведомления (подписчиками), и объектом, запускающим событие (издателем). Эта система событий позволяет программе определять специфичные события, которые могут передавать пользовательские аргументы, содержащие значения, необходимые подписчику. Идея здесь заключается в том, чтобы избежать прямой зависимости между подписчиком и издателем.
Вот пример того, как можно использовать Publish/Subscribe, при наличии необходимой функциональной реализации publish()
, subscribe()
и unsubscribe()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// A very simple new mail handler // A count of the number of messages received var mailCounter = 0; // Initialize subscribers that will listen out for a topic // with the name "inbox/newMessage". // Render a preview of new messages var subscriber1 = subscribe( "inbox/newMessage", function( topic, data ) { // Log the topic for debugging purposes console.log( "A new message was received: ", topic ); // Use the data that was passed from our subject // to display a message preview to the user $( ".messageSender" ).html( data.sender ); $( ".messagePreview" ).html( data.body ); }); // Here's another subscriber using the same data to perform // a different task. // Update the counter displaying the number of new // messages received via the publisher var subscriber2 = subscribe( "inbox/newMessage", function( topic, data ) { $('.newMessageCounter').html( ++mailCounter ); }); publish( "inbox/newMessage", [{ sender: "hello@google.com", body: "Hey there! How are you doing today?" }]); // We could then at a later point unsubscribe our subscribers // from receiving any new topic notifications as follows: // unsubscribe( subscriber1 ); // unsubscribe( subscriber2 ); |
Общая идея здесь — наличие свободной связи. Подписчики подписываются на конкретную задачу другого объекта, получая уведомления в ходе выполнения.
Преимущества
Паттерны Observer и Publish/Subscribe заставляют нас задуматься о взаимоотношениях между различными частями нашего приложения. Они помогают нам определить, какие слои содержат прямые отношения, которые можно заменить на субъекты и наблюдатели. Это позволяет разбить приложение на мелкие куски, которые будут минимально связаны с другими частями нашегов кода, что улучшит управление и предоставит возможность повторно использовать код.
Мотивация использования шаблона Observer заключается в том, что нам нужно поддерживать согласованность лишь между связанными объектами, не делая классы зависимыми друг от друга. Например в случаях, когда объект должен иметь возможность уведомлять другие объекты, не делая никаких предположений, что это за объекты.
Динамические отношения могут существовать между наблюдателями и субъектами и при использовании иных паттернов. Однако это не так просто реализовать, когда разные части нашего приложения тесно связаны.
Несмотря на то, Observer и и Publish/Subscribe не всегда является идеальным решением, всё же эти шаблоны остаются одними из лучших инструментов для разработки развязанных систем и должны рассматриваться как важный инструмент в списке утилит разработчика JavaScript.
Недостатки
Некоторые проблемы паттернов связаны с их основными преимуществами. В Publish/Subscribe, отделяя издателей от подписчиков, иногда бывает трудно получить гарантии того, что определенные части наших приложений функционируют корректно.
Еще одна обратная сторона шаблона заключается в том, что подписчики совершенно не знают о существовании друг друга. Это может повысить стоимость нашего кода. Из-за динамических отношений между подписчиками и издателями порою зависимость от обновлений может стать трудно отслеживаемой.
Это была первая часть достаточно большой главы книги Эдди. Во второй части мы более детально рассмотрим имплементацию Publish/Subscribe.