После перехода на JavaScript из мира PHP при более глубоком изучении механизмов работы импортирования модулей в JavaScript я задался вопросом: а не является ли это реализацией принципа Dependency Injection (далее DI), который устоялся как хорошая практика в разработке ПО, или может быть это даже какая-то альтернатива?

В ООП общепризнанно, что принцип DI – эффективный прием проектирования слабо связанных между собой компонентов приложения. Но JavaScript – это абсолютно другая вселенная. Из-за того, что в JavaScript есть возможность пропатчить что угодно прямо на лету (Monkey Patching) используя простые техники, разработчикам пришлось адаптировать некоторые техники для написания больших и масштабируемых приложений. Теперь подавляющее большинство разработчиков создают небольшие и даже крошечные модули, которые публикуют свой функционал через export одновременно скрывая свои приватные функции от перезаписи другими разработчиками. И, беря на вооружение преимущество модульности, можно получить одновременно как сокрытый от изменений код, так и код, который очень легко тестировать в дальнейшем.

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

Импортирование модулей с require

В обычном JavaScript с require() это выглядит примерно так:

const someModule = require('./someModule')

module.exports = function doSomethingWithRequest() {
  // do stuff
  someModule.someFunc()
  // do other stuff
}

Плюсы:

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

Минусы:

  • Долгий запуск, так как при старте выполняется весь достигаемый код.
  • Сложная подмена при тестировании. Но CommonJS (и даже AMD) позволяют на лету подменять импортируемые модули, например при помощи mockery или rewire.
  • На больших проектах получается много кода в тестах, потому что абсолютно в каждом файле придется импортировать модуль, к примеру, users, от которого зависит много других модулей. Т.е. в тестах для каждого из зависимых модулей придется работать с экземпляром модуля users.
  • Редактирование всех зависимых файлов при перемещении/рефакторинге. Не так больно для TypeScript и какой-нибудь умный IDE типа WebStorm/PhpStorm.
  • Предыдущий пункт, можно сказать, противоречит принципу DI - потому что мы импортируем конкретный модуль, а не его интерфейс. (уточнить, прочитать)

Инъекция зависимостей

Это выглядит примерно так:

module.exports = function doSomethingWithRequest(someModule) {
  // do stuff
  someModule.someFunc()
  // do other stuff
}

Плюсы:

  • Удобное тестирование: легко замокать или подменить someModule, даже не прибегая к сторонним инструментам. Достаточно вызвать функцию doSomethingWithRequest указав в аргументах нужный нам объект.
  • Это уже ближе к правильному принципу DI. А именно – инъекция зависимостей через конструктор.

Минусы:

  • Необходима тонкая настройка во многих проектах
  • Необходимо внедрение Dependency Injection Container (далее DIC) для более простого эксплуатирования, иначе ручное управление графом зависимостей может быстро разрастись и усложниться.

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

Заключение

Я бы рекомендовал для любого проекта, который потенциально нужно будет поддерживать от двух лет, с выкаткой даже небольших фич хотя бы раз в неделю, использовать принцип DI с DIC , так как никто не знает насколько широко разрастется ваш проект. Но подключить библиотеку типа awilix на начальном этапе не займет много времени, зато сразу получаете более тестируемый код за счет легкой подмены любой компоненты приложения. Безусловно, это не серебряная пуля и поддерживаемость кода зависит не только от тестируемости, но и от мастерства разработчика, сроков и т.д. Но, как показывает практика – если код сложно протестировать, значит его сложно и поддерживать.

Кстати, что думаете насчет такого?

export function foo({secretModule = require('./secret-module.js')}) {};

Использованные материалы:

Node.js advanced pattern: Dependency Injection Container
In this post, I’m going over the Dependency Injection Container (DIC), which is an advanced module wiring pattern used by the majority of applications and libraries in Node.js. This pattern is…
r/javascript - Module Requiring vs Dependency Injection in Javascript
8 votes and 7 comments so far on Reddit
Inversion of Control vs Dependency Injection
According to the paper written by Martin Fowler, inversion of control is the principle where the control flow of a program is inverted: instead of the programmer controlling the flow of a program, ...