Всем привет, читатели Хабра! Меня зовут Владислав, я frontend-разработчик в компании Nord Clan. В этой статье я собираюсь простыми словами рассказать про паттерн Наблюдатель и как он используется в Redux. Также хотел бы обратить внимание на то, что статья ориентирована для новичков, однако может быть полезной для более опытных коллег.
Наблюдатель — поведенческий шаблон проектирования. Также известен как «подчинённые». Реализует у класса механизм, который позволяет объекту этого класса получать оповещения об изменении состояния других объектов и тем самым наблюдать за ними.
Знакомство с паттерном
Итак, что же из себя представляет наш наблюдатель(observer) и какую проблему он решает?
Начнем с примера из жизни: подписка на новостную ленту, либо подписка на почтовую рассылку. Допустим, Издатель — это объект который публикует что-то интересное и важное, Подписчик — тот кто следит за этими обновлениями и в зависимости от оповещения Издателя(Observable) выполняет свои действия.
Для небольших проектов и проектов средней сложности бывает происходит достаточно частая проблема — при непрерывном выполнении задач и реализации нового функционала без уделения должного внимания на архитектуру приложения, появляется высокая связанность компонентов в коде, которая делает в будущем любое изменение в проекте достаточно проблематичным из за постоянно нарастающей взаимосвязи различных объектов.
Такую проблему в частности может решить как раз паттерн наблюдателя, разрывая сильную связь между объектами и делая ее слабосвязанной.
Если два объекта могут взаимодействовать, не обладая практически никакой информацией друг о друге, такие объекты называют слабосвязанными.
Паттерн Наблюдатель определяет отношение «один-ко-многим» между объектами таким образом, что при изменении состояния одного объекта происходит автоматическое оповещение и обновление всех зависимых объектов.
Как раз то что нужно, для того чтобы улучшить ситуацию на проекте или изначально заложить правильный фундамент в приложении с нуля. Хотя зачастую проблема решится уже заранее готовой библиотекой в которой будет реализован этот паттерн, будет полезно иметь навык реализовать его на чистом языке для собственных нужд.
Внизу на схемах показана обобщенная реализация паттерна:
Теперь, закончив с общими абстрактными описаниями, перейдем ближе к конкретике и языку, все примеры будут приведены на javascript, читатели знакомые с другими языками программирования без труда поймут что здесь происходит. Также в качестве визуального примера продемонстрирую примерную схему которая уже приближена к практической реализации:
Первое из чего будет состоять паттерн — это основной класс в котором будет происходить вся «магия» вычислений:
class Observable {
observers = [];
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers.filter((o) => o !== observer);
}
notify(payload) {
this.observers.forEach((observer) => observer(payload));
}
}
export default Observable;
Давайте поэтапно разберем что делает каждый метод в данном классе:
У нас есть базовый класс, назовем его также Observervable(наблюдаемым).
Первое с чего начнем — это определим переменную, где будем хранить будущих подписчиков в виде массива:
observers = [];
В методе subscribe реализуется механизм оформления подписки на объект Observer и добавление наблюдаемого компонента в массив подписчиков:
class Observable {
//...
subscribe(observer) {
this.observers.push(observer);
}
}
По аналогии с методом subscribe реализуем и метод отписки:
class Observable {
//...
unsubscribe(observer) {
this.observers.filter((o) => o !== observer);
}
}
В методе уведомления наблюдателей проходимся по всему массиву подписчиков и передаем им через параметр необходимую информацию:
class Observable {
//...
notify(payload) {
this.observers.forEach((observer) => observer(payload));
}
}
Данная тройка методов, описанных сверху, обязательна для правильной реализации паттерна, в дальнейшем на них будут построены методы бизнес-логики.
Теперь на основе этого класса реализуем практическое применение в виде рассылки события всем слушателям нажатием по кнопке:
class Observer {
subscribers = [];
constructor() {
if (!Observer.instance) {
Observer.instance = this;
}
return Observer.instance;
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
return this;
}
unsubscribe(subscriber) {
this.subscribers.filter(sub => sub !== subscriber);
return this;
}
notify(payload) {
this.subscribers.forEach(subscriber => subscriber(payload));
return this;
}
}
// определим первого слушателя
function logToConsole(message) {
console.log(message);
}
// и второго
function logToDom(message) {
const logsContainer = document.getElementById('observer-logs');
logsContainer.innerHTML += `<li>${message}</li>`;
}
const btn = document.getElementById('btn');
const observer = new Observer();
// подписываем двух слушателей на observer
const subscribers = [logToConsole, logToDom];
subscribers.forEach(subscriber => observer.subscribe(subscriber));
// выполняем оповещение при нажатии на кнопку
btn.addEventListener('click', e => {
e.preventDefault();
observer.notify('btn clicked');
})
Вкратце вот как выглядит в общих чертах паттерн на простом примере, теперь же рассмотрим как он реализован внутри Redux.
Сравнение методов Observable и Redux
Чтобы понять как связаны методы класса наблюдателя и реализация их в Redux, взглянем на такие элементы Redux как store, action, dispatch, но reducer в данной статье я не упоминаю из-за того что он не принадлежит к паттерну и просто считается преобразующей функцией, передаваемой в параметры хранилища.
Давайте пройдемся по всем составляющим этой реализации.
Store — главный элемент хранилища состояния. Будучи основной функцией Redux, он возвращает дерево состояния и в нем же реализованы методы подписки и уведомления:
function store(reducer) {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
dispatch({ type: '@@redux/INIT' });
return {
getState,
dispatch,
subscribe
};
};
getState возвращает текущее состояние дерева в приложении:
const getState = () => state;
Метод dispatch, аналог в чистой реализации —notify(payload):
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
Обновляем состояние, передав в reducer текущее состояние и action:
state = reducer(state, action);
Эта строка кода выполняет перечисление всех подписчиков и вызывает каждого слушателя через listener(), уведомляя что состояние изменилось:
listeners.forEach(listener => listener());
Метод subscribe позволяет нам подписаться на обновление состояния, а callback в качестве аргумента в этом случае становится слушателем(listener)
который выполнится всякий раз, когда состояние хранилища будет обновлено.
Стоит отметить, что в практической разработке для обновления UI в реакте используются готовые решения в виде react-redux библиотеки и вместо метода subscribe применяется метод mapStateToProps():
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
Также наряду с подпиской, в redux реализована отписка от наблюдателя, вызывается она не отдельным методом как в классе, а через return, который вернет массив с отфильтрованными слушателями и уберет ненужный:
return () => {
listeners = listeners.filter(l => l !== listener);
};
При создании хранилища отправляется действие «INIT», которое служит для того чтобы установить начальное общее содержимое состояния приложения.
dispatch({ type: '@@redux/INIT' });
Связывание Redux и UI (на примере с React)
Наконец, хотелось бы привести также небольшой упрощенный пример классового компонента чтобы показать как выглядит связывание Redux в UI и какие методы помещаются в жизненные циклы:
class Counter extends React.Component {
componentDidMount() {
const {subscribe} = this.props.store;
this.unsubscribe = subscribe(this.forceUpdate);
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
const {getState, dispatch} = this.props.store;
return (
<div>
<p>{getState().count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
Increment counter
</button>
</div>
);
}
}
Достаем метод subscribe из хранилища передающегося через props, далее подписываем наш компонент через this.forceUpdate, это ручной способ вызвать метод render в компоненте:
componentDidMount() {
const {subscribe} = this.props.store;
this.unsubscribe = subscribe(this.forceUpdate);
}
также задаем отписку и помещаем ее в жизненный компонент, где происходит размонтирование компонента:
componentWillUnmount() {
this.unsubscribe();
}
В методе render достаем оставшиеся методы и размещаем их в jsx разметке где getState будет предоставлять всегда актуальные данные состояния напрямую из хранилища, а dispatch привязывается к событию нажатия кнопки и вызывает выполнение всех остальных методов:
render() {
const {getState, dispatch} = this.props.store;
return (
<div>
<p>{getState().count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>
Increment counter
</button>
</div>
);
}
Заключение
На этом у меня все. Всем спасибо кто дочитал статью до конца, надеюсь информация была полезной как и для новичков, так и для опытных разработчиков и смогла открыть новое видение на казалось бы уже знакомый стейт-менеджер через пример реализации паттерна.
P.S. В качестве кода примеров использовались наработки из репозитория
Комментарии (6)
strannik_k
07.11.2022 21:33В Redux используется паттерн издатель-подписчик. В вашей статье описан тоже он.
Паттерн наблюдатель немного другой. По ссылке описана разница между ними:
https://medium.com/clean-code-channel/observer-vs-pub-sub-pattern-4fc1da35d11funca
07.11.2022 23:51+2В pub-sub паблишер и получатель не имеют ссылок друг на друга, коммуникация идёт чисто с помощью данных - сообщений.
В редаксе стор в этом месте знает всех своих подписчиков в лицо и сам же вызывает их на каждый свой чих. Т.е это observable в чистом виде.
Просто у паттерна название имхо не очень удачное, поскольку активная роль здесь у observable, который и дирижирует всем этим оркестром из observer'ов.
melkor_morgoth
08.11.2022 14:50Да, как раз https://medium.com/clean-code-channel/observer-vs-pub-sub-pattern-4fc1da35d11 описано, что для издателя нужен какой-либо промежуточный компонент, который будет перенаправлять события издателя нужным подписчикам, то есть этот компонент скрывает детали функциональности модулей-подписчиков и издатель тем самым не взаимодействует напрямую с этими модулями. Возможно, он будет посложнее в реализации чем Наблюдатель...
dyadyaSerezha
08.11.2022 00:07+1В тексте, картинках и примерах взаимозаменямо используется слова observer и observable, хотя на самом деле они противоположные. Нехорошо.
markelov69
Зачем обрамлять Observer/Observable таком Г как Redux?? Уже же давным давно всё придумано, причем по человечески, с использованием getters/setters и это называется MobX. Ради приличия могли бы для демонстрации мощи Observer/Observable реализовать самую простую версию MobX и observer для реакт компонентов.
mayorovp
Ну да, mobx всяко лучше. Если забыть о том, что примером паттерна "Наблюдатель" mobx не является :-)