Объяснение паттерна Наблюдатель на примере Redux

Функция Frodo надела кольцо на палец и увидела его, class Observer. Горящее око взывало к Frodo...Функция Frodo надела кольцо на палец и увидела его, class Observer. Горящее око взывало к Frodo…

Всем привет, читатели Хабра! Меня зовут Владислав, я 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 += `
  • ${message}
  • `; } 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 (
          

    {getState().count}

    ); } }

    Достаем метод 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 (
          

    {getState().count}

    ); }

    Заключение

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

    P.S. В качестве кода примеров использовались наработки из репозитория

    © Habrahabr.ru