Полное руководство по шаблонам проектирования JavaScript

Опубликовано: 2022-03-11

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

Полное руководство по шаблонам проектирования JavaScript

В течение жизни любого языка многие такие многоразовые решения создаются и тестируются большим количеством разработчиков из сообщества этого языка. Именно из-за этого объединенного опыта многих разработчиков такие решения настолько полезны, потому что они помогают нам писать код оптимизированным способом, в то же время решая стоящую перед нами проблему.

Основные преимущества, которые мы получаем от шаблонов проектирования, заключаются в следующем:

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

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

Краткая история JavaScript

Сегодня JavaScript — один из самых популярных языков программирования для веб-разработки. Первоначально он был сделан как своего рода «клей» для различных отображаемых HTML-элементов, известный как язык сценариев на стороне клиента, для одного из первых веб-браузеров. Названный Netscape Navigator, в то время он мог отображать только статический HTML. Как вы можете предположить, идея такого языка сценариев привела к браузерным войнам между крупными игроками в индустрии разработки браузеров того времени, такими как Netscape Communications (сегодня Mozilla), Microsoft и другими.

Каждый из крупных игроков хотел протолкнуть свою собственную реализацию этого языка сценариев, поэтому Netscape создала JavaScript (на самом деле это сделал Брендан Эйх), Microsoft создала JScript и так далее. Как вы можете себе представить, различия между этими реализациями были велики, поэтому разработка для веб-браузеров производилась для каждого браузера с наиболее просматриваемыми наклейками, которые прилагались к веб-странице. Вскоре стало понятно, что нам нужно стандартное, кроссбраузерное решение, которое унифицирует процесс разработки и упростит создание веб-страниц. То, что они придумали, называется ECMAScript.

ECMAScript — это стандартизированная спецификация языка сценариев, которую стараются поддерживать все современные браузеры, и существует несколько реализаций (можно сказать, диалектов) ECMAScript. Самый популярный из них — тема этой статьи, JavaScript. С момента своего первоначального выпуска ECMAScript стандартизировал множество важных вещей, и для тех, кто больше интересуется спецификой, в Википедии есть подробный список стандартизированных элементов для каждой версии ECMAScript. Поддержка браузерами ECMAScript версии 6 (ES6) и выше все еще неполная, и для полной поддержки ее необходимо перенести в ES5.

Что такое JavaScript?

Чтобы полностью понять содержание этой статьи, давайте познакомимся с некоторыми очень важными языковыми характеристиками, о которых нам нужно знать, прежде чем углубляться в шаблоны проектирования JavaScript. Если бы кто-то спросил вас: «Что такое JavaScript?» вы могли бы ответить где-нибудь в строках:

JavaScript — это легкий, интерпретируемый, объектно-ориентированный язык программирования с первоклассными функциями, наиболее известный как язык сценариев для веб-страниц.

Вышеупомянутое определение означает, что код JavaScript имеет небольшой объем памяти, прост в реализации и изучении, а его синтаксис аналогичен популярным языкам, таким как C++ и Java. Это язык сценариев, что означает, что его код интерпретируется, а не компилируется. Он поддерживает процедурный, объектно-ориентированный и функциональный стили программирования, что делает его очень гибким для разработчиков.

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

JavaScript поддерживает первоклассные функции

Раньше мне было трудно понять эту характеристику, когда я только начинал с JavaScript, так как я имел опыт работы с C/C++. JavaScript рассматривает функции как граждан первого класса, что означает, что вы можете передавать функции в качестве параметров другим функциям так же, как и любую другую переменную.

 // we send in the function as an argument to be // executed from inside the calling function function performOperation(a, b, cb) { var c = a + b; cb(c); } performOperation(2, 3, function(result) { // prints out 5 console.log("The result of the operation is " + result); })

JavaScript основан на прототипе

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

Только сейчас, в ES6, вводится формальный термин class , что означает, что браузеры до сих пор его не поддерживают (если вы помните, на момент написания последней полностью поддерживаемой версией ECMAScript была 5.1). Однако важно отметить, что хотя термин «класс» введен в JavaScript, внутри него по-прежнему используется наследование на основе прототипов.

Программирование на основе прототипов — это стиль объектно-ориентированного программирования, в котором повторное использование поведения (известное как наследование) выполняется посредством процесса повторного использования существующих объектов посредством делегирования, которые служат прототипами. Мы углубимся в это более подробно, как только перейдем к разделу статьи о шаблонах проектирования, поскольку эта характеристика используется во многих шаблонах проектирования JavaScript.

Циклы событий JavaScript

Если у вас есть опыт работы с JavaScript, вы наверняка знакомы с термином функция обратного вызова . Для тех, кто не знаком с этим термином, функция обратного вызова — это функция, отправляемая в качестве параметра (помните, JavaScript рассматривает функции как объекты первого класса) другой функции и выполняется после возникновения события. Обычно это используется для подписки на такие события, как щелчок мышью или нажатие кнопки на клавиатуре.

Графическое изображение цикла событий JavaScript

Каждый раз, когда событие, к которому подключен слушатель, срабатывает (в противном случае событие теряется), сообщение отправляется в очередь сообщений, которые обрабатываются синхронно в порядке FIFO (первым пришел — первым обслужен). ). Это называется циклом событий .

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

 while (queue.waitForMessage()) { queue.processNextMessage(); }

Функция queue.waitForMessage() синхронно ожидает новых сообщений. Каждое из обрабатываемых сообщений имеет свой собственный стек и обрабатывается до тех пор, пока стек не станет пустым. После его завершения из очереди обрабатывается новое сообщение, если оно есть.

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

Что такое шаблоны проектирования?

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

Протопаттерны

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

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

Как вы можете узнать, что то, что, как вам кажется, вы распознали, на самом деле является шаблоном проектирования?

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

Прото-шаблон является будущим, если он проходит определенный период тестирования различными разработчиками и сценариями, в которых шаблон оказывается полезным и дает правильные результаты. Предстоит проделать довольно большой объем работы и документации, большая часть которых выходит за рамки этой статьи, чтобы сделать полноценный паттерн, признанный сообществом.

Антипаттерны

Как шаблон проектирования представляет собой хорошую практику, так и анти-шаблон представляет собой плохую практику.

Примером антишаблона может быть изменение прототипа класса Object . Почти все объекты в JavaScript наследуются от Object (помните, что JavaScript использует наследование на основе прототипа), поэтому представьте себе сценарий, в котором вы изменили этот прототип. Изменения в прототипе Object будут видны во всех объектах, наследуемых от этого прототипа , то есть в большинстве объектов JavaScript . Это катастрофа, ожидающая своего часа.

Другой пример, похожий на упомянутый выше, — это изменение объектов, которыми вы не владеете. Примером этого может быть переопределение функции объекта, используемого во многих сценариях в приложении. Если вы работаете с большой командой, представьте, какую путаницу это вызовет; вы быстро столкнетесь с коллизиями имен, несовместимыми реализациями и кошмарами обслуживания.

Подобно тому, как полезно знать обо всех хороших практиках и решениях, также очень важно знать и о плохих. Таким образом, вы можете распознать их и избежать ошибки заранее.

Категоризация шаблонов проектирования

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

  • Креативные шаблоны проектирования
  • Шаблоны структурного проектирования
  • Шаблоны поведенческого дизайна
  • Шаблоны параллельного проектирования
  • Шаблоны архитектурного проектирования

Креативные шаблоны проектирования

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

  • Заводской метод
  • Абстрактная фабрика
  • Строитель
  • Прототип
  • Синглтон

Шаблоны структурного проектирования

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

  • Адаптер
  • Мост
  • Композитный
  • Декоратор
  • Фасад
  • Наилегчайший вес
  • Прокси

Шаблоны поведенческого проектирования

Эти типы шаблонов распознают, реализуют и улучшают связь между разрозненными объектами в системе. Они помогают обеспечить синхронизацию информации в разрозненных частях системы. Популярные примеры таких шаблонов:

  • Цепочка ответственности
  • Команда
  • Итератор
  • Медиатор
  • Сувениры
  • Наблюдатель
  • Состояние
  • Стратегия
  • Посетитель

Шаблоны параллельного проектирования

Эти типы шаблонов проектирования имеют дело с парадигмами многопоточного программирования. Вот некоторые из популярных:

  • Активный объект
  • Ядерная реакция
  • Планировщик

Шаблоны архитектурного проектирования

Шаблоны проектирования, которые используются в архитектурных целях. Некоторые из самых известных из них:

  • MVC (модель-представление-контроллер)
  • MVP (модель-представление-ведущий)
  • MVVM (Модель-Вид-ВидМодель)

В следующем разделе мы более подробно рассмотрим некоторые из вышеупомянутых шаблонов проектирования с примерами, приведенными для лучшего понимания.

Примеры шаблонов проектирования

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

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

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

Шаблон конструктора

Говоря о классических объектно-ориентированных языках, конструктор — это специальная функция в классе, которая инициализирует объект с некоторым набором значений по умолчанию и/или отправленных значений.

Распространенными способами создания объектов в JavaScript являются три следующих способа:

 // either of the following ways can be used to create a new object var instance = {}; // or var instance = Object.create(Object.prototype); // or var instance = new Object();

После создания объекта есть четыре способа (начиная с ES3) добавить свойства к этим объектам. Они следующие:

 // supported since ES3 // the dot notation instance.key = "A key's value"; // the square brackets notation instance["key"] = "A key's value"; // supported since ES5 // setting a single property using Object.defineProperty Object.defineProperty(instance, "key", { value: "A key's value", writable: true, enumerable: true, configurable: true }); // setting multiple properties using Object.defineProperties Object.defineProperties(instance, { "firstKey": { value: "First key's value", writable: true }, "secondKey": { value: "Second key's value", writable: false } });

Самый популярный способ создания объектов — фигурные скобки, а для добавления свойств — запись через точку или квадратные скобки. Любой, у кого есть опыт работы с JavaScript, использовал их.

Ранее мы упоминали, что JavaScript не поддерживает собственные классы, но поддерживает конструкторы за счет использования ключевого слова «новое» перед вызовом функции. Таким образом, мы можем использовать функцию в качестве конструктора и инициализировать ее свойства так же, как в конструкторе классического языка.

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; this.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

Тем не менее, здесь еще есть возможности для улучшения. Если вы помните, я упоминал ранее, что JavaScript использует наследование на основе прототипов. Проблема с предыдущим подходом заключается в том, что метод writesCode переопределяется для каждого из экземпляров конструктора Person . Мы можем избежать этого, установив метод в прототип функции:

 // we define a constructor for Person objects function Person(name, age, isDeveloper) { this.name = name; this.age = age; this.isDeveloper = isDeveloper || false; } // we extend the function's prototype Person.prototype.writesCode = function() { console.log(this.isDeveloper? "This person does write code" : "This person does not write code"); } // creates a Person instance with properties name: Bob, age: 38, isDeveloper: true and a method writesCode var person1 = new Person("Bob", 38, true); // creates a Person instance with properties name: Alice, age: 32, isDeveloper: false and a method writesCode var person2 = new Person("Alice", 32); // prints out: This person does write code person1.writesCode(); // prints out: this person does not write code person2.writesCode();

Теперь оба экземпляра конструктора Person могут получить доступ к общему экземпляру метода writesCode() .

Шаблон модуля

Что касается особенностей, то JavaScript не перестает удивлять. Еще одна особенность JavaScript (по крайней мере, в отношении объектно-ориентированных языков) заключается в том, что JavaScript не поддерживает модификаторы доступа. В классическом языке ООП пользователь определяет класс и определяет права доступа для его членов. Поскольку JavaScript в своей простой форме не поддерживает ни классы, ни модификаторы доступа, разработчики JavaScript придумали способ имитировать это поведение, когда это необходимо.

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

 // we used an immediately invoked function expression // to create a private variable, counter var counterIncrementer = (function() { var counter = 0; return function() { return ++counter; }; })(); // prints out 1 console.log(counterIncrementer()); // prints out 2 console.log(counterIncrementer()); // prints out 3 console.log(counterIncrementer());

Как видите, с помощью IIFE мы привязали переменную счетчика к функции, которая была вызвана и закрыта, но все еще может быть доступна дочерней функции, которая увеличивает ее. Поскольку мы не можем получить доступ к переменной счетчика из-за пределов функционального выражения, мы сделали ее частной с помощью манипуляций с областью действия.

Используя замыкания, мы можем создавать объекты с приватными и общедоступными частями. Они называются модулями и очень полезны, когда мы хотим скрыть определенные части объекта и предоставить интерфейс только пользователю модуля. Покажем это на примере:

 // through the use of a closure we expose an object // as a public API which manages the private objects array var collection = (function() { // private members var objects = []; // public members return { addObject: function(object) { objects.push(object); }, removeObject: function(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } }, getObjects: function() { return JSON.parse(JSON.stringify(objects)); } }; })(); collection.addObject("Bob"); collection.addObject("Alice"); collection.addObject("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(collection.getObjects()); collection.removeObject("Alice"); // prints ["Bob", "Franck"] console.log(collection.getObjects());

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

Однако не все так идеально. Если вы хотите изменить видимость члена, вам нужно изменить код везде, где вы использовали этот член, из-за разного характера доступа к общедоступной и частной частям. Кроме того, методы, добавленные к объекту после его создания, не могут получить доступ к закрытым членам объекта.

Выявление шаблона модуля

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

 // we write the entire object logic as private members and // expose an anonymous object which maps members we wish to reveal // to their corresponding public members var namesCollection = (function() { // private members var objects = []; function addObject(object) { objects.push(object); } function removeObject(object) { var index = objects.indexOf(object); if (index >= 0) { objects.splice(index, 1); } } function getObjects() { return JSON.parse(JSON.stringify(objects)); } // public members return { addName: addObject, removeName: removeObject, getNames: getObjects }; })(); namesCollection.addName("Bob"); namesCollection.addName("Alice"); namesCollection.addName("Franck"); // prints ["Bob", "Alice", "Franck"] console.log(namesCollection.getNames()); namesCollection.removeName("Alice"); // prints ["Bob", "Franck"] console.log(namesCollection.getNames());

Раскрывающий шаблон модуля — это один из по крайней мере трех способов реализации модульного шаблона. Различия между шаблоном раскрывающегося модуля и другими вариантами шаблона модуля заключаются, прежде всего, в том, как ссылаются на общедоступные члены. В результате шаблон раскрывающегося модуля намного проще в использовании и модификации; однако в некоторых сценариях он может оказаться ненадежным, например при использовании объектов RMP в качестве прототипов в цепочке наследования. Проблемные ситуации следующие:

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

Синглтон шаблон

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

 var singleton = (function() { // private singleton value which gets initialized only once var config; function initializeConfiguration(values){ this.randomNumber = Math.random(); values = values || {}; this.number = values.number || 5; this.size = values.size || 10; } // we export the centralized method for retrieving the singleton value return { getConfig: function(values) { // we initialize the singleton value only once if (config === undefined) { config = new initializeConfiguration(values); } // and return the same config value wherever it is asked for return config; } }; })(); var configObject = singleton.getConfig({ "size": 8 }); // prints number: 5, size: 8, randomNumber: someRandomDecimalValue console.log(configObject); var configObject1 = singleton.getConfig({ "number": 8 }); // prints number: 5, size: 8, randomNumber: same randomDecimalValue as in first config console.log(configObject1);

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

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

Шаблон наблюдателя

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

Существуют различные версии этого паттерна, но в его самой основной форме у нас есть две основные части паттерна. Первый — субъект, а второй — наблюдатели.

Субъект обрабатывает все операции, связанные с определенной темой, на которую подписаны наблюдатели. Эти операции подписывают наблюдателя на определенную тему, отменяют подписку наблюдателя на определенную тему и уведомляют наблюдателей об определенной теме при публикации события.

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

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

Я собираюсь показать краткий пример шаблона издатель/подписчик, но для тех, кто заинтересован, пример классического шаблона наблюдателя можно легко найти в Интернете.

 var publisherSubscriber = {}; // we send in a container object which will handle the subscriptions and publishings (function(container) { // the id represents a unique subscription id to a topic var id = 0; // we subscribe to a specific topic by sending in // a callback function to be executed on event firing container.subscribe = function(topic, f) { if (!(topic in container)) { container[topic] = []; } container[topic].push({ "id": ++id, "callback": f }); return id; } // each subscription has its own unique ID, which we use // to remove a subscriber from a certain topic container.unsubscribe = function(topic, id) { var subscribers = []; for (var subscriber of container[topic]) { if (subscriber.id !== id) { subscribers.push(subscriber); } } container[topic] = subscribers; } container.publish = function(topic, data) { for (var subscriber of container[topic]) { // when executing a callback, it is usually helpful to read // the documentation to know which arguments will be // passed to our callbacks by the object firing the event subscriber.callback(data); } } })(publisherSubscriber); var subscriptionID1 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Bob's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID2 = publisherSubscriber.subscribe("mouseHovered", function(data) { console.log("I am Bob's callback function for a hovered mouse event and this is my event data: " + JSON.stringify(data)); }); var subscriptionID3 = publisherSubscriber.subscribe("mouseClicked", function(data) { console.log("I am Alice's callback function for a mouse clicked event and this is my event data: " + JSON.stringify(data)); }); // NOTE: after publishing an event with its data, all of the // subscribed callbacks will execute and will receive // a data object from the object firing the event // there are 3 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"}); // we unsubscribe from an event by removing the subscription ID publisherSubscriber.unsubscribe("mouseClicked", subscriptionID3); // there are 2 console.logs executed publisherSubscriber.publish("mouseClicked", {"data": "data1"}); publisherSubscriber.publish("mouseHovered", {"data": "data2"});

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

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

Паттерн посредника

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

Медиатор — это объект, который используется в качестве центральной точки для связи между разрозненными частями системы и управляет рабочим процессом между ними. Теперь важно подчеркнуть, что он обрабатывает рабочий процесс. Почему это важно?

Потому что есть большое сходство с шаблоном издатель/подписчик. Вы можете спросить себя: хорошо, значит, эти два шаблона помогают реализовать лучшую связь между объектами… В чем разница?

Разница в том, что посредник управляет рабочим процессом, тогда как издатель/подписчик использует так называемый тип связи «запустил и забыл». Издатель/подписчик — это просто агрегатор событий, то есть он просто заботится об срабатывании событий и сообщает нужным подписчикам, какие события были запущены. Агрегатор событий не заботится о том, что происходит после запуска события, чего нельзя сказать о посреднике.

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

Таким образом, код будет намного чище (его будет легче поддерживать), и пользователь не будет перегружен объемом информации, которая запрашивается только для завершения регистрации. Медиатор — это объект, который будет обрабатывать этапы регистрации, принимая во внимание различные возможные рабочие процессы, которые могут возникнуть из-за того, что у каждого пользователя потенциально может быть уникальный процесс регистрации.

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

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

Образец прототипа

Как мы уже упоминали в статье, JavaScript не поддерживает классы в своей родной форме. Наследование между объектами реализовано с помощью программирования на основе прототипов.

Это позволяет нам создавать объекты, которые могут служить прототипом для других создаваемых объектов. Объект-прототип используется в качестве схемы для каждого объекта, создаваемого конструктором.

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

 var personPrototype = { sayHi: function() { console.log("Hello, my name is " + this.name + ", and I am " + this.age); }, sayBye: function() { console.log("Bye Bye!"); } }; function Person(name, age) { name = name || "John Doe"; age = age || 26; function constructorFunction(name, age) { this.name = name; this.age = age; }; constructorFunction.prototype = personPrototype; var instance = new constructorFunction(name, age); return instance; } var person1 = Person(); var person2 = Person("Bob", 38); // prints out Hello, my name is John Doe, and I am 26 person1.sayHi(); // prints out Hello, my name is Bob, and I am 38 person2.sayHi();

Take notice how prototype inheritance makes a performance boost as well because both objects contain a reference to the functions which are implemented in the prototype itself, instead of in each of the objects.

Command Pattern

The command pattern is useful in cases when we want to decouple objects executing the commands from objects issuing the commands. For example, imagine a scenario where our application is using a large number of API service calls. Then, let's say that the API services change. We would have to modify the code wherever the APIs that changed are called.

This would be a great place to implement an abstraction layer, which would separate the objects calling an API service from the objects which are telling them when to call the API service. This way, we avoid modification in all of the places where we have a need to call the service, but rather have to change only the objects which are making the call itself, which is only one place.

As with any other pattern, we have to know when exactly is there a real need for such a pattern. We need to be aware of the tradeoff we are making, as we are adding an additional abstraction layer over the API calls, which will reduce performance but potentially save a lot of time when we need to modify objects executing the commands.

 // the object which knows how to execute the command var invoker = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; } } // the object which is used as an abstraction layer when // executing commands; it represents an interface // toward the invoker object var manager = { execute: function(name, args) { if (name in invoker) { return invoker[name].apply(invoker, [].slice.call(arguments, 1)); } return false; } } // prints 8 console.log(manager.execute("add", 3, 5)); // prints 2 console.log(manager.execute("subtract", 5, 3));

Facade Pattern

The facade pattern is used when we want to create an abstraction layer between what is shown publicly and what is implemented behind the curtain. It is used when an easier or simpler interface to an underlying object is desired.

A great example of this pattern would be selectors from DOM manipulation libraries such as jQuery, Dojo, or D3. You might have noticed using these libraries that they have very powerful selector features; you can write in complex queries such as:

 jQuery(".parent .child div.span")

It simplifies the selection features a lot, and even though it seems simple on the surface, there is an entire complex logic implemented under the hood in order for this to work.

We also need to be aware of the performance-simplicity tradeoff. It is desirable to avoid extra complexity if it isn't beneficial enough. In the case of the aforementioned libraries, the tradeoff was worth it, as they are all very successful libraries.

Next Steps

Design patterns are a very useful tool which any senior JavaScript developer should be aware of. Knowing the specifics regarding design patterns could prove incredibly useful and save you a lot of time in any project's lifecycle, especially the maintenance part. Modifying and maintaining systems written with the help of design patterns which are a good fit for the system's needs could prove invaluable.

In order to keep the article relatively brief, we will not be displaying any more examples. For those interested, a great inspiration for this article came from the Gang of Four book Design Patterns: Elements of Reusable Object-Oriented Software and Addy Osmani's Learning JavaScript Design Patterns . I highly recommend both books.

Связанный: Как разработчик JS, это то, что не дает мне спать по ночам / Разбираемся в путанице классов ES6