
Всем привет, читатели Хабра! Меня зовут Владислав, я frontend-разработчик в компании Nordclan. В этой статье я собираюсь простыми словами рассказать про паттерн Наблюдатель и как он используется в 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. В качестве кода примеров использовались наработки из репозитория
ссылка на оригинал статьи https://habr.com/ru/company/nordclan/blog/697026/
Добавить комментарий