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

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

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

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

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

  • Модуль
  • Прототип
  • Наблюдатель
  • Синглтон

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

  1. Контекст: где и при каких обстоятельствах используется шаблон?
  2. Проблема: что мы пытаемся решить?
  3. Решение: как использование этого шаблона решает предложенную нами проблему?
  4. Реализация. Как выглядит реализация?

Шаблон дизайна модуля

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

Для тех, кто знаком с объектно-ориентированными языками, модули представляют собой «классы» JavaScript. Одним из многих преимуществ классов является инкапсуляция - защита состояний и поведения от доступа из других классов. Шаблон модуля допускает общедоступный и частный (плюс менее известные защищенные и привилегированные) уровни доступа.

Модули должны быть Immediately-Invoked-Function-Expressions (IIFE), чтобы разрешить частные области видимости, то есть закрытие, которое защищает переменные и методы (однако оно вернет объект вместо функции). Вот как это выглядит:

(function() {
    // declare private variables and/or functions
    return {
      // declare public variables and/or functions
    }
})();

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

var HTMLChanger = (function() {
  var contents = 'contents'
  var changeHTML = function() {
    var element = document.getElementById('attribute-to-change');
    element.innerHTML = contents;
  }
  return {
    callChangeHTML: function() {
      changeHTML();
      console.log(contents);
    }
  };
})();
HTMLChanger.callChangeHTML();       // Outputs: 'contents'
console.log(HTMLChanger.contents);  // undefined

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

Выявление паттерна модуля

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

var Exposer = (function() {
  var privateVariable = 10;
  var privateMethod = function() {
    console.log('Inside a private method!');
    privateVariable++;
  }
  var methodToExpose = function() {
    console.log('This is a method I want to expose!');
  }
  var otherMethodIWantToExpose = function() {
    privateMethod();
  }
  return {
      first: methodToExpose,
      second: otherMethodIWantToExpose
  };
})();
Exposer.first();        // Output: This is a method I want to expose!
Exposer.second();       // Output: Inside a private method!
Exposer.methodToExpose; // undefined

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

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

Любой разработчик JavaScript либо видел ключевое слово прототип, сбитый с толку прототипным наследованием, либо реализовал прототипы в своем коде. Шаблон проектирования Прототип основан на прототипном наследовании JavaScript. Модель прототипа используется в основном для создания объектов в ситуациях, требующих высокой производительности.

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

Шаблон дизайна прототипа в Википедии

Этот UML описывает, как интерфейс прототипа используется для клонирования конкретных реализаций.

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

var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}

TeslaModelS.prototype.go = function() {
  // Rotate wheels
}

TeslaModelS.prototype.stop = function() {
  // Apply brake pads
}

Конструктор позволяет создать один объект TeslaModelS. При создании нового объекта TeslaModelS он сохранит состояния, инициализированные в конструкторе. Кроме того, поддерживать функции go и stop очень просто, поскольку мы объявили их с помощью prototype. Синонимичный способ расширения функций прототипа, как описано ниже:

var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}

TeslaModelS.prototype = {
  go: function() {
    // Rotate wheels
  },
  stop: function() {
    // Apply brake pads
  }
}

Выявление паттерна прототипа

Подобно паттерну «Модуль», паттерн «Прототип» также имеет показательную вариацию. Выявление шаблона прототипа обеспечивает инкапсуляцию с общедоступными и закрытыми членами, поскольку он возвращает литерал объекта.

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

var TeslaModelS = function() {
  this.numWheels    = 4;
  this.manufacturer = 'Tesla';
  this.make         = 'Model S';
}
TeslaModelS.prototype = function() {
  var go = function() {
    // Rotate wheels
  };
  var stop = function() {
    // Apply brake pads
  };
  return {
    pressBrakePedal: stop,
    pressGasPedal: go
  }
}();

Обратите внимание, как функции stop and go будут защищены от возвращаемого объекта из-за того, что они находятся вне области действия возвращаемого объекта. Поскольку JavaScript изначально поддерживает прототипное наследование, нет необходимости переписывать базовые функции.

Шаблон проектирования наблюдателя

Часто бывает, что одна часть приложения изменяется, а другие части нуждаются в обновлении. В AngularJS, если объект $scope обновляется, может быть инициировано событие для уведомления другого компонента. Шаблон наблюдателя включает именно это - если объект изменен, он передает зависимым объектам информацию о том, что изменение произошло.

Другой яркий пример - архитектура модель-представление-контроллер (MVC); Вид обновляется при изменении модели. Одно из преимуществ - отделение представления от модели для уменьшения зависимостей.

Шаблон дизайна наблюдателя в Википедии

Как показано на схеме UML, необходимыми объектами являются объекты subject, observer и concrete. Тема содержит ссылки на конкретных наблюдателей для уведомления о любых изменениях. Объект Observer - это абстрактный класс, который позволяет конкретным наблюдателям реализовать метод уведомления.

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

// Controller 1
$scope.$on('nameChanged', function(event, args) {
    $scope.name = args.name;
});
...
// Controller 2
$scope.userNameChanged = function(name) {
    $scope.$emit('nameChanged', {name: name});
};

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

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

Мы можем создавать наши собственные Субъекты и Наблюдатели на JavaScript. Посмотрим, как это реализовано:

var Subject = function() {
  this.observers = [];
  return {
    subscribeObserver: function(observer) {
      this.observers.push(observer);
    },
    unsubscribeObserver: function(observer) {
      var index = this.observers.indexOf(observer);
      if(index > -1) {
        this.observers.splice(index, 1);
      }
    },
    notifyObserver: function(observer) {
      var index = this.observers.indexOf(observer);
      if(index > -1) {
        this.observers[index].notify(index);
      }
    },
    notifyAllObservers: function() {
      for(var i = 0; i < this.observers.length; i++){
        this.observers[i].notify(i);
      };
    }
  };
};
var Observer = function() {
  return {
    notify: function(index) {
      console.log("Observer " + index + " is notified!");
    }
  }
}
var subject = new Subject();
var observer1 = new Observer();
var observer2 = new Observer();
var observer3 = new Observer();
var observer4 = new Observer();
subject.subscribeObserver(observer1);
subject.subscribeObserver(observer2);
subject.subscribeObserver(observer3);
subject.subscribeObserver(observer4);
subject.notifyObserver(observer2); // Observer 2 is notified!
subject.notifyAllObservers();
// Observer 1 is notified!
// Observer 2 is notified!
// Observer 3 is notified!
// Observer 4 is notified!

Опубликовать / подписаться

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

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

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

В AngularJS подписчик «подписывается» на событие с помощью $ on («событие», обратный вызов), а издатель «публикует» событие с помощью $ emit («событие», аргументы) или $ broadcast («событие», аргументы). .

Синглтон

Синглтон допускает создание только одного экземпляра, но множества экземпляров одного и того же объекта. Синглтон запрещает клиентам создавать несколько объектов, после создания первого объекта он будет возвращать экземпляры самого себя.

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

var printer = (function () {
  var printerInstance;
  function create () {
    function print() {
      // underlying printer mechanics
    }
    function turnOn() {
      // warm up
      // check for paper
    }
    return {
      // public + private states and behaviors
      print: print,
      turnOn: turnOn
    };
  }
  return {
    getInstance: function() {
      if(!printerInstance) {
        printerInstance = create();
      }
      return printerInstance;
    }
  };
  function Singleton () {
    if(!printerInstance) {
      printerInstance = intialize();
    }
  };
})();

Метод create является частным, потому что мы не хотим, чтобы клиент имел к нему доступ, однако обратите внимание, что метод getInstance является общедоступным. Каждый сотрудник-офицер может сгенерировать экземпляр принтера, взаимодействуя с методом getInstance, например:

var officePrinter = printer.getInstance();

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

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

Вывод

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

Перед созданием любого приложения вы должны тщательно подумать о каждом актере и о том, как они взаимодействуют друг с другом. Изучив шаблоны проектирования Module, Prototype, Observer и Singleton, вы сможете идентифицировать эти шаблоны и использовать их в реальных условиях.