Шаблон Модуль в JavaScript

Spread the love

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

Прим. переводчика
Отличия Модуля от Singlenton
Шаблон Модуля в Javascript используется для создания модульности кода с общим механизмом взаимодействия . Он хорошо работает для разделения одного «класса» на несколько файлов, поскольку вы можете независимо определить конструктор и различные группы методов-прототипов. Каждый из модулей может быть заключен в замыкание для создания статических локальных переменных – это называется раскрытием шаблона модуля.
Паттерн Singlenton в javascript относится к ограничению создания экземпляров, часто с использованием ленивой инициализации.

Определение Модуля

Модуль создается как IIFE (immediately invoked function expression – выражение для немедленного вызова функции) с функцией внутри:

const SomeModule = (function() {})();

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

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

Давайте попробуем создать модуль с приватной функцией внутри.

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
})();

Как видите, есть простая функция log, которая выводит полученное сообщение в консоль. Как ее выполнить? Formatter.log?

Formatter.log("Hello");

Можете ли вы угадать, что произойдет далее? Uncaught TypeError: Cannot read property ‘log’ of undefined. Почему так? Поскольку наш модуль ничего не возвращает, то для внешнего мира в нем фактически ничего не определено, даже если код внутри будет выполняться.

const Formatter = (function() {
  console.log("Start");
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
})();

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

Итак, теперь мы знаем, что доступ к модулю – это доступ к тому, что он возвращает.

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

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };
})();

Так! Это еще одна функция в модуле, к которой я не могу получить доступ!

Раскрытие модуля

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

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };  

  return {
    makeUppercase,
  }
})();

Теперь мы можем использовать функцию makeUppercase, как обычно:

console.log(Formatter.makeUppercase("tomek"));

Какой будет результат?

> Start
> [1551191285526] Logger: Making uppercase
> TOMEK

Модуль может содержать не только функции, но и массивы, объекты и примитивы.

const Formatter = (function() {
  let timesRun = 0;

  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);
  const setTimesRun = () => { 
    log("Setting times run");
    ++timesRun;
  }

  const makeUppercase = (text) => {
    log("Making uppercase");
    setTimesRun();
    return text.toUpperCase();
  };

  return {
    makeUppercase,
    timesRun,
  }
})();

Давайте выполним это код:

console.log(Formatter.makeUppercase("tomek"));
console.log(Formatter.timesRun);

Как и ожидается, отобразиться 0. Но обратите внимание, что на это можно повлиять извне.

Formatter.timesRun = 10;
console.log(Formatter.timesRun);

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

Объявление зависимостей модуля

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

Для этого у Модуля могут быть зависимости. Давайте попробуем написать функцию, которая будет писать сообщение в наш запрошенный HTML-элемент.

const Formatter = (function() {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    document.querySelector(selector).innerHTML = message;
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})();

Formatter.writeToDOM("#target", "Hi there");

Это будет работать при условии, что у нас есть элемент с идентификатором target в нашем DOM. Звучит отлично, но объект document доступен только тогда, когда доступен DOM. Запуск кода на сервере Node приведет к ошибке. Итак, как сделать так, чтобы у нас все было хорошо?

Один из вариантов – проверить, существует ли объект document.

const writeToDOM = (selector, message) => {
  if (!!document && "querySelector" in document) {
    document.querySelector(selector).innerHTML = message;
  }
}

И этот код в значительной степени обо всем позаботится, но мне он не нравится. Теперь Модуль действительно зависит от чего-то извне. Это сценарий «Я пойду, только если мой друг тоже пойдет». Так не должно быть.

Мы можем объявить зависимости нашего Модуля и внедрить их.

const Formatter = (function(doc) {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    if (!!doc && "querySelector" in doc) {
      doc.querySelector(selector).innerHTML = message;
    }
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})(document);

Рассмотрим это шаг за шагом. Вверху есть аргумент doc у нашей функции. Затем он используется в методе writeToDOM вместо document. В конце, прямо в последней строке, мы добавляем document. Почему? Это и есть тот самый аргумент, с которым будет вызываться наш модуль. Почему я изменил имя аргумента на doc в модуле? Я не люблю путаницу с переменными.

Теперь у нас есть прекрасная конструкция для тестирования. Вместо того, чтобы полагаться на то, есть ли в наших инструментах тестирования симулятор DOM или что-то подобное, мы можем вставить макет. Но нам нужно вставить его во время нашего определения, а не позже. Это довольно просто, вам просто нужно написать макет и указать его как «запасного»:

const documentMock = (() => ({
  querySelector: (selector) => ({
    innerHTML: null,
  }),
}))();

const Formatter = (function(doc) {
  const log = (message) => console.log(`[${Date.now()}] Logger: ${message}`);

  const makeUppercase = (text) => {
    log("Making uppercase");
    return text.toUpperCase();
  };

  const writeToDOM = (selector, message) => {
    doc.querySelector(selector).innerHTML = message;
  }

  return {
    makeUppercase,
    writeToDOM,
  }
})(document || documentMock);

Теперь можно убрать проверку внутри makeUppercase, потому что она больше не нужна.

Шаблон модуля очень распространен и, как вы можете видеть, очень удобен. Я часто стараюсь сначала писать модули, а потом – если нужно – классы.

Оригинальная статья: TomekModule pattern in JavaScript

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

Spread the love
Подписаться
Уведомление о
guest
1 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Вячелсав
Вячелсав
4 лет назад

Модульную систему node.js типо module.exports, require (‘….’) по сути тоже можно отнести к данному шаблону?