Шаблоны проектирования JavaScript: Шаблон Observer

Spread the love

В JavaScript часто возникает подобная задача. Необходимо создать авто обновление части страницы в ответ на определенные события, используя данные, которые они предоставляют. Скажем, например, пользовательский ввод, который вы затем проецируете на один или несколько компонентов. Это приводит к большому количеству push-and-pull в коде, чтобы все синхронизировать.

Это самый очевидный пример того, где может помочь шаблон проектирования observer (наблюдатель). Этот шаблон позволяет связывать данные «один ко многим» между элементами. Эта односторонняя привязка данных может управляться событиями.

В этой статье я хотел бы рассмотреть шаблон проектирования observer. Он поможет вам решить частую проблему, создания связи «один ко многим», «односторонняя и управляемая событиями». Это проблема, которая часто возникает, когда у вас много элементов, которые должны быть синхронизированы.

Я буду использовать ECMAScript 6 для создания примеров иллюстрации шаблона. Еще будут классы, стрелочные функции и константы. Не стесняйтесь изучить эти темы самостоятельно, если вы еще с ними не знакомы. Так же я буду следовать Test-Driven-Development (TDD) походу. Таким образом, у нас будет способ узнать, насколько полезен каждый компонент.

Итак, начнем.

Наблюдатель за событиями

Общий вид шаблона выглядит следующим образом:

EventObserver
│ 
├── subscribe: добавляет новые наблюдаемые события
│ 
├── unsubscribe: удаляет наблюдаемые события
|
└── broadcast: выполняет все события со связанными данными

После того, как я создан шаблон наблюдателя, я добавлю пример его использования с компонентом подсчетом количество слов.

Создадим и инициализируем EventObserver:

class EventObserver {
  constructor() {
    this.observers = [];
  }
}

Начнем с пустого списка наблюдаемых событий observers для каждого нового экземпляра. Далее добавим еще методов в EventObserver в соответствие с шаблоном проектирования.

Метод Subscribe

Добавим новый метод subscribe:

subscribe(fn) {
  this.observers.push(fn);
}

Размещаем новый элемент в массив наблюдаемых событий. Список событий представляет собой список функций обратного вызова.

Один из способов проверить этот метод:

// Arrange
const observer = new EventObserver();
const fn = () => {};

// Act
observer.subscribe(fn);

// Assert
assert.strictEqual(observer.observers.length, 1);

Тут я использую функцию Node assert для тестирования этого компонента. Точно такие же assertions существуют и в Chai assertions.

Обратите внимание, что список наблюдаемых событий состоит из обратных вызовов. Затем мы проверяем длину списка и проверяем, что обратный вызов находится в списке.

Метод Unsubscribe

Сделаем следующий метод для удаления события:

unsubscribe(fn) {
  this.observers = this.observers.filter((subscriber) => subscriber !== fn);
}

Отфильтруем из списка все, что соответствует функции обратного вызова. Если совпадений нет, обратный вызов остается в списке. Фильтр возвращает новый список и переназначает список наблюдателей.

Тестирование метода:

// Arrange
const observer = new EventObserver();
const fn = () => {};

observer.subscribe(fn);

// Act
observer.unsubscribe(fn);

// Assert
assert.strictEqual(observer.observers.length, 0);

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

Метод Broadcast

Следующий метод для вызова всех событий:

broadcast(data) {
  this.observers.forEach((subscriber) => subscriber(data));
}

Здесь мы перебираем список наблюдаемых событий и выполняет все обратные вызовы. Благодаря этому вы получаете необходимое отношение «один ко многим» к подписанным событиям. Так же здесь передается параметр data, который связывает данные обратного вызова.

ES6 делает код более эффективным с помощью стрелочной функции. Обратите внимание на функцию (subscriber) => subscriber(data), которая выполняет большую часть работы.

Проверим метод broadcast:

// Arrange
const observer = new EventObserver();

let subscriberHasBeenCalled = false;
const fn = (data) => subscriberHasBeenCalled = data;

observer.subscribe(fn);

// Act
observer.broadcast(true);

// Assert
assert(subscriberHasBeenCalled);

Этот тест позволяет мне убедиться, в том что наблюдатель работает так, как я ожидаю.

Таким образом мы создали EventObserver. Вопрос в том, что нам с ним делать дальше?

Шаблон Observer в действии: демонстрация подсчета количества слов в блоге

Для демонстрации, мы будем автоматически подсчитывать слова в тексте поста. Каждое нажатие клавиши, которое вы вводите в качестве ввода, будет вызывать подсчет слов в помощью шаблона EventObserver.

Чтобы получить количество слов из свободного ввода текста, можно сделать сделующее:

const getWordCount = (text) => text ? text.trim().split(/\s+/).length : 0;

В этой, казалось бы, простой функции происходит много всего. Далее напишем модульный тест.

// Arrange
const blogPost = 'This is a blog \n\n  post with a word count.     ';

// Act
const count = getWordCount(blogPost);

// Assert
assert.strictEqual(count, 9);

Обратите внимание на несколько странную строку ввода внутри blogPost. Я намерен, чтобы эта функция охватывала как можно больше крайних случаев. Пока это дает мне правильное количество слов, значит мы движемся в правильном направлении.

Время подключить эти компоненты к DOM. Это та часть, где мы можете использовать простой JavaScript и встроить его прямо в браузер.

Добавим следующий HTML-код на страницу:

<textarea id="blogPost" placeholder="Enter your blog post..." class="blogPost">
</textarea>

Вслед за ним JavaScript:

const wordCountElement = document.createElement('p');

wordCountElement.className = 'wordCount';
wordCountElement.innerHTML = 'Word Count: <strong id="blogWordCount">0</strong>';

document.body.appendChild(wordCountElement);

const blogObserver = new EventObserver();

blogObserver.subscribe((text) => {
  const blogCount = document.getElementById('blogWordCount');

  blogCount.textContent = getWordCount(text);
});

const blogPost = document.getElementById('blogPost');

blogPost.addEventListener('keyup', () => blogObserver.broadcast(blogPost.value));

Этот код позволит отслеживать изменения в textarea и подсчитывать количество слов прямо под ней. Я использую body.appendChild() в DOM API, чтобы добавить новый элемент. Затем event listeners, чтобы воплотить идею в жизнь.

Обратите внимание, что с помощью стрелочных функций можно связать события одной строкой. () => BlogObserver.broadcast() выполняет большую часть работы. Он передает последние изменения в текстовую область прямо в функцию обратного вызова.

Ни одна демонстрация не будет полной без той, которую вы можете потрогать и настроить, поэтому ниже приведена ссылка на CodePen:

Я бы не назвал этот пример завершенным. Это всего лишь отправная точка для изучения шаблона Наблюдатель. На мой взгляд, вопрос, как далеко вы готовы пойти?

Дальнейшее развитие

Это зависит от вас, чтобы развить эту идею еще дальше. Есть много способов использовать шаблон наблюдателя для создания новых функций.

Вы можете добавим в демо:

  • Еще один компонент, который считает количество абзацев
  • Еще один компонент, который показывает предварительный просмотр введенного текста
  • Или улучшить предварительный просмотр с поддержкой markdown

Это всего лишь несколько идей, которые вы можете сделать, чтобы глубже погрузиться в это.

Заключение

Шаблон проектирования Наблюдатель может помочь вам решить реальные проблемы в JavaScript. Он решает проблему синхронизации множества элементов с одними и теми же данными. Как часто бывает, когда браузер запускает определенные события. Я уверен, что большинство из вас уже столкнулись с такой проблемой и столкнулись с инструментами и сторонними зависимостями.

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

Эта статья была рецензирована Giulio Mainardi.

Оригинальная статья: Camilo ReyesJavaScript Design Patterns: The Observer Pattern

Была ли вам полезна эта статья?
[0 / 0]

Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован.