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

Всем привет, читатели Хабра! Меня зовут Владислав, я 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)


  1. markelov69
    07.11.2022 19:12

    Зачем обрамлять Observer/Observable таком Г как Redux?? Уже же давным давно всё придумано, причем по человечески, с использованием getters/setters и это называется MobX. Ради приличия могли бы для демонстрации мощи Observer/Observable реализовать самую простую версию MobX и observer для реакт компонентов.


    1. mayorovp
      08.11.2022 15:59
      -1

      Ну да, mobx всяко лучше. Если забыть о том, что примером паттерна "Наблюдатель" mobx не является :-)


  1. strannik_k
    07.11.2022 21:33

    В Redux используется паттерн издатель-подписчик. В вашей статье описан тоже он.

    Паттерн наблюдатель немного другой. По ссылке описана разница между ними:
    https://medium.com/clean-code-channel/observer-vs-pub-sub-pattern-4fc1da35d11


    1. funca
      07.11.2022 23:51
      +2

      В pub-sub паблишер и получатель не имеют ссылок друг на друга, коммуникация идёт чисто с помощью данных - сообщений.

      В редаксе стор в этом месте знает всех своих подписчиков в лицо и сам же вызывает их на каждый свой чих. Т.е это observable в чистом виде.

      Просто у паттерна название имхо не очень удачное, поскольку активная роль здесь у observable, который и дирижирует всем этим оркестром из observer'ов.


      1. melkor_morgoth
        08.11.2022 14:50

        Да, как раз https://medium.com/clean-code-channel/observer-vs-pub-sub-pattern-4fc1da35d11 описано, что для издателя нужен какой-либо промежуточный компонент, который будет перенаправлять события издателя нужным подписчикам, то есть этот компонент скрывает детали функциональности модулей-подписчиков и издатель тем самым не взаимодействует напрямую с этими модулями. Возможно, он будет посложнее в реализации чем Наблюдатель...


  1. dyadyaSerezha
    08.11.2022 00:07
    +1

    В тексте, картинках и примерах взаимозаменямо используется слова observer и observable, хотя на самом деле они противоположные. Нехорошо.