Ещё одна имплементация Dependency Injection в JavaScript — с ES6-модулями, с возможностью использовать один и тот же код в браузере, и в nodejs и не использовать транспиляторы.


image


Под катом — мой взгляд на DI, его место в современных web-приложениях, принципиальная реализация DI-контейнера, способного создавать объекты и на фронте, и на бэке, а также объяснение, при чём тут Майкл Джексон.


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


Объекты в приложении


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


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


  • постоянные — возникают на каком-то этапе работы приложения и уничтожаются только при завершении приложения;
  • временные — возникают при необходимости выполнения некоторой операции и уничтожаются при завершении выполнения этой операции;

В связи с этим в программировании есть такие шаблоны проектирования, как:



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


Контейнер Объектов


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


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


Место внедрения


По большому счёту есть два способа внедрить зависимости в объект:


  • через конструктор;
  • через свойство (или его акцессор);

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


Допустим, что у нас есть приложение, состоящее из трёх объектов:


image


В PHP (этот язык с давними традициями DI у меня в данный момент находится в активном багаже, к JS я перейду чуть позже) подобная ситуация могла бы быть отражена таким образом:


class Config
{
    public function __construct()
    {
    }
}

class Service
{
    private $config;

    public function __construct(Config $config)
    {
        $this->config = $config;
    }
}

class Application
{
    private $config;
    private $service;

    public function __construct(Config $config, Service $service)
    {
        $this->config = $config;
        $this->service = $service;
    }
}

Этой информации должно хватать, чтобы DI-контейнер (например, league/container) при соответствующей настройке смог по запросу на создание объекта Application также создать его зависимости Service и Config и передать их параметрами в конструктор объекта Application.


Идентификаторы зависимостей


Каким же образом Контейнер объектов понимает, что конструктору объекта Application требуются два объекта Config и Service? Путём анализа объекта через Reflection API (Java, PHP) или через анализ непосредственно кода объекта (аннотаций к коду). То есть, в общем случае, мы можем определить имена переменных, которые ожидает увидеть на входе конструктор объекта, а если язык типизируемый, то можем получить также и типы этих переменных.


Таким образом, в качестве идентификаторов объектов Контейнер может оперировать либо именами входных параметров конструктора, либо типами входных параметров.


Создание объектов


Объект может быть в явном виде создан программистом и помещён в Контейнер под соответствующим идентификатором (например, "configuration")


/** @var \League\Container\Container $container */
$container->add("configuration", $config);

а может быть создан Контейнером по некоторым определённым правилам. Эти правила, по большому счёту, сводятся к сопоставлению идентификатора объекта его коду. Правила можно задавать явно (маппинг в виде кода, XML, JSON, ...)


[
  ["object_id_1", "/path/to/source1.php"],
  ["object_id_2", "/path/to/source2.php"],
  ...
]

или в виде некоторого алгоритма:


public function getSource($id)
{.
    return "/path/to/source/${id}.php";
}

В PHP составление правил сопоставления имени класса файлу с его исходным кодом стандартизированы (PSR-4), в Java сопоставление идёт на уровне конфигурации JVM (class loader). Если Контейнер предусматривает автоматический поиск исходников при создании объектов, то имена классов являются достаточно хорошими идентификаторами для объектов в таком Контейнере.


Namespaces


Обычно в проекте, помимо собственного кода, используются также сторонние модули. С появлением менеджеров зависимостей (maven, composer, npm) использование модулей очень сильно упростилось, а количество модулей в проектах очень сильно увеличилось. Пространства имён позволяют существовать в едином проекте одноимённым элементам кода из различных модулей (классы, функции, константы).


Есть языки, в которых пространство имён встроено изначально (Java):


package vendor.project.module.folder;

есть языки, в которых пространство имён добавлено в ходе развития языка (PHP):


namespace Vendor\Project\Module\Folder;

Хорошая реализация пространства имён позволяет однозначно адресовать любой элемент кода:


\Doctrine\Common\Annotations\Annotation\Attribute::$name

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


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


$container->add(\Vendor\Project\Module\ObjectType::class, $obj);

Автозагрузка кода


В PHP composer пространство имён модуля маппится на файловую систему внутри модуля в дескрипторе модуля composer.json:


"autoload": {
    "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" }
}

JS-сообщество могло бы делать аналогичный маппинг в package.json, если бы в JS были пространства имён.


Идентификаторы зависимостей в JS


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


  1. JS — язык с динамической типизацией и не предусматривает указание типов при объявлении функции.
  2. В JS используются минификаторы, которые могут переименовывать входные параметры.

Разработчики DI-контейнера awilix предлагают использовать объект в качестве единственного входного параметра конструктора, а в качестве зависимостей — свойства этого объекта:


class UserController {
  constructor(opts) {
    this.userService = opts.userService
  }
}

Идентификатор свойства объекта в JS может состоять из буквенно-цифровых символов, "_" и "$", причем не может начинаться с цифры.


Так как нам для автозагрузки нужно будет мапить идентификаторы зависимостей на путь к их исходникам в файловой системе, то лучше отказаться от использования "$" и воспользоваться опытом PHP. До появления оператора namespace в некоторых framework'ах (например, в Zend 1) использовали такие наименования для классов:


class Zend_Config_Writer_Json {...}

Таким образом, мы могли бы отразить наше приложение из трёх объектов (Application, Config, Service) на JS как-то так:


class Vendor_Project_Config {
    constructor() {
    }
}

class Vendor_Project_Service {
    constructor({Vendor_Project_Config}) {
        this.config = Vendor_Project_Config;
    }
}

class Vendor_Project_Application {
    constructor({Vendor_Project_Config, Vendor_Project_Service}) {
        this.config = Vendor_Project_Config;
        this.service = Vendor_Project_Service;
    }
}

Если мы размещаем код каждого класса:


export default class Vendor_Project_Application {
    constructor({Vendor_Project_Config, Vendor_Project_Service}) {
        this.config = Vendor_Project_Config;
        this.service = Vendor_Project_Service;
    }
}

в своём файле внутри модуля нашего проекта:


  • ./src/
    • ./Application.js
    • ./Config.js
    • ./Service.js

То мы можем связать корневой каталог модуля с корневым "namespace'ом" модуля в конфигурации Контейнера:


const ns = "Vendor_Project";
const path = path.join(module_root, "src"); 
container.addSourceMapping(ns, path);

а затем, отталкиваясь от этой информации, конструировать на основании идентификатора зависимости (Vendor_Project_Config) путь к соответствующим исходникам (${module_root}/src/Config.js).


ES6 Модули


ES6 предлагает общую конструкцию для загрузки ES6-модулей:


import { something } from 'path/to/source/with/something';

Так как нам нужно один объект (класс) привязывать к одному файлу, то есть смысл в исходнике экспортировать этот класс по-умолчанию:


export default class Vendor_Project_Path_To_Source_With_Something {...}

В принципе, можно не писать такое длинное имя для класса, достаточно просто Something и тоже будет работать, но в Zend 1 писали и не переломились, а уникальность имени класса в пределах проекта положительно сказывается как на возможностях IDE (autocomplete и контекстные подсказки), так и при отладке:


image


Импорт класса и создание объекта в таком случае выглядит так:


import Something from 'path/to/source/with/something';
const something = new Something();

Front & Back импорт


Импорт работает как в браузере, так и в nodejs, но есть нюансы. Например, браузер не понимает импорта nodejs-модулей:


import path from "path";

В браузере получаем ошибку:


Failed to resolve module specifier "path". Relative references must start with either "/", "./", or "../".

То есть, если мы хотим, чтобы наш код работал и в браузере, и в nodejs, мы не можем использовать конструкции которые не понимает браузер или nodejs. Я специально акцентирую на этом внимание, потому что такой вывод слишком естественен, чтобы о нём думать. Как дышать.


Место DI в современных web-приложениях


Это сугубо моё личное мнение, обусловленное моим персональным опытом, как и всё остальное в этой публикации.


В web-приложениях JS практически безальтернативно занимает своё место на фронте, в браузере. На серверной стороне плотно окопались Java, PHP, .Net, Ruby, python,… Но с появлением nodejs JavaScript также проник и на сервер. А технологии, используемые в других языках, в том числе и DI, начали проникать в серверный JS.


Развитие JavaScript обусловлено асинхроностью работы кода в браузере. Асинхронность не является исключительной особенностью JS, скорее врождённой. Сейчас наличие JS и на сервере, и на фронте уже никого не удивляет, а скорее, стимулирует к использованию одних и тех же подходов на обоих "концах" web-приложения. И одного и того же кода. Разумеется, что фронт и бэк слишком различаются по своей сути и по решаемым задачам, чтобы использовать один и тот же код и там, и там. Но можно предположить, что в более-менее сложном приложении будет код браузерный, серверный и общий.


DI уже сейчас используется на фронте, в RequireJS:


define(
    ["./config", "./service"],
    function App(Config, Service) {}
);

Правда тут идентификаторы зависимостей прописываются в явном виде и сразу в виде ссылок на исходники (можно настроить маппинг идентификаторов в конфиге загрузчика).


В современных web-приложениях DI существует не только на серверной стороне, но и в браузере.


При чём тут Майкл Джексон?


При включении поддержки ES-модулей в nodejs (флаг --experimental-modules) движок идентифицирует содержимое файлов с расширением *.mjs как EcmaScript-модули (в отличие от Common-модулей с расширением *.cjs).


Иногда такой подход называют "Michael Jackson Solution", а скрипты — Michael Jackson Scripts (*.mjs).


Согласен, что так себе интрига с КДПВ разрешилась, но… камон ребят, Майкл Джексон...


Yet Another DI Implementation


Ну и как полагается, собственный велосипед DI-модуль — @teqfw/di


Это не готовое "к бою" решение, а скорее принципиальная реализация. Все зависимости должны представлять из себя ES-модули и использовать общие для браузера и nodejs возможности.


Для разрешения зависимостей в модуле применяется подход awilix:


constructor(spec) {
    /** @type {Vendor_Module_Config} */
    const _config = spec.Vendor_Module_Config;
    /** @type {Vendor_Module_Service} */
    const _service = spec.Vendor_Module_Service;
}

Для запуска back-примера:


import Container from "./src/Container.mjs";
const container = new Container();
container.addSourceMapping("Vendor_Module", "../example");
container.get("Vendor_Module_App")
    .then((app) => {
        app.run();
    });

на сервере:


$ node --experimental-modules main.mjs

Для запуска front-примера (example.html):


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>DI in Browser</title>
    <script type="module" src="./main.mjs"></script>
</head>
<body>
<p>Load main script './main.mjs', create new DI container, then get object by ID from container.</p>
<p>Open browser console to see output.</p>
</body>
</html>

нужно выложить модуль на сервер и открыть страницу example.html в браузере (или воспользоваться возможностями IDE). Если открывать example.html напрямую, то в Chrom'е ошибка:


Access to script at 'file:///home/alex/work/teqfw.di/main.mjs' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, https.

Если всё прошло удачно, то в консоли (браузера или nodejs) будет примерно такой вывод:


Create object with ID 'Vendor_Module_App'.
Create object with ID 'Vendor_Module_Config'.
There is no dependency with id 'Vendor_Module_Config' yet.
'Vendor_Module_Config' instance is created.
Create object with ID 'Vendor_Module_Service'.
There is no dependency with id 'Vendor_Module_Service' yet.
'Vendor_Module_Service' instance is created (deps: [Vendor_Module_Config]).
'Vendor_Module_App' instance is created (deps: [Vendor_Module_Config, Vendor_Module_Service]).
Application 'Vendor_Module_Config' is running.

Резюме


AMD, CommonJS, UMD?


ESM!

Комментарии (39)


  1. symbix
    21.08.2019 23:37
    +2

    Ха, какая изящная маскировка Service Locator-а под DI. Даже может показаться, что это DI! :-)


    1. flancer Автор
      22.08.2019 06:25

      А чем, по вашему, true DI отличается от Service Locator, замаскированного под DI?


      1. VolCh
        22.08.2019 06:46

        Доступом к контейнеру


        1. flancer Автор
          22.08.2019 07:17

          Поясните, пожалуйста, свою версию различий на примере. Очень не хотелось бы получить вместо DI-контейнера контейнер Service Locator'а (весь интернет забит информацией, что это жуткий анти-паттерн).


          Может ещё что-то можно исправить, кода-то в реализации совсем немного — 250 строк где-то. Ну или переимновать di-модуль в sl-модуль, если исправление принципиально невозможно.


          1. VolCh
            22.08.2019 12:53

            Я исхожу из того, что в коде


            constructor(spec) {
                /** @type {Vendor_Module_Config} */
                const _config = spec.Vendor_Module_Config;
                /** @type {Vendor_Module_Service} */
                const _service = spec.Vendor_Module_Service;
            }

            spec иметт множество полей, а не исключительно Vendor_Module_Service и Vendor_Module_Config. Ну вот как-то сложилось такое впечатление. Если spec генерируется исключительно для этого класса, какждый раз когда нужен его инстанс, то таки ближе к true DI


            1. flancer Автор
              22.08.2019 13:24

              Это просто я привёл более классическую запись объекта со свойствами, можно конструктор записать и так:


              constructor({Vendor_Module_Config, Vendor_Module_Service}) {
                  const _config = Vendor_Module_Config;
                  const _service = Vendor_Module_Service;
              }

              можно даже не вводить промежуточные константы, а напрямую обращаться к:


              export default class Vendor_Module_App {
                  constructor({Vendor_Module_Config, Vendor_Module_Service}) {
                      this.name = "Vendor_Module_App";
                      this.run = function () {
                          console.log(`Application '${this.name}' is running with deps: [${Vendor_Module_Config.name}, ${Vendor_Module_Service.name}].`);
                      }
                  }
              }


      1. symbix
        22.08.2019 16:01
        +1

        Изящность (я тут без иронии) вашего варианта в том, что вы "взломали" мой стандартный способ объяснения, чем DI отличается от SL:


        DI:


        class SomeClass
        {
             constructor(private foo: Foo, private bar: Bar) {}
        }

        SL:


        class SomeClass
        {
             private foo: Foo;
             private bar: Bar;
        
             constructor(container: Container) {
                 this.foo = container.foo;
                 this.bar = container.bar;
             }
        }

        Но, в принципе, оно справедливо и с вашим вариантом — ведь за элегантным трюком стоит именно второе.


        Если в целом — то в DI контейнер решает, что именно заинжектить, а в SL класс решает, что к себе втащить.


        Если детально — DI позволяет:
        1) создать всю цепочку зависимостей рекурсивно "по требованию" (в принципе, это и в вашем варианте решаемо через геттеры или proxy),
        2) завязываться на интерфейсы, а не на конкретные реализации (это, в принципе, у вас тоже можно),
        3) поддерживать разные варианты инстанциирования (вот тут синглтон, а вот тут новый инстанс на каждое обращение) — это, в принципе, тоже у вас можно (см.п.1), но — только глобально (см. п. 5),
        4) сохранить возможность прямого создания объекта безо всяких там контейнеров, тупо написав new Foo(dep1, dep2) — ну, с поправкой на лишние скобочки, это, положим, есть,
        5) инжектить разные реализации в зависимости от контекста (Foo и Bar хотят CacherInterface, в Foo я хочу MemcachedCacher, а в Bar я хочу RedisCacher), либо сделать "везде синглтон, но вот для Baz — новый инстанс" — вот тут уже облом,
        6) подсовывать в конструктор иные аргументы, которые не зависимости (скажем, есть какой-нибудь аргумент defaultTimeout у http-клиента) — тоже облом (хотя это сомнительная фича).


        И, да, как уже справедливо заметили, любой DI можно использовать как SL (в конце концов, даже при правильном использовании как минимум один раз — во входной точке — он именно так и используется). Но не наоборот.


        1. flancer Автор
          22.08.2019 16:28
          +1

          Примерно понял. Спасибо. Но в том-то и дело, что сам контейнер в классы не передаётся. Т.е., нет такого:


          constructor(container: Container)

          spec — это примитивный объект, который в качестве properties содержит зависимости конструируемого объекта:


          {
              dep1: <Object1>,
              dep2: <Object2>,
              ...
          }

          Контейнер берёт конструктор объекта, а затем запускает процесс создания объекта, передавая в конструктор прокси-объект, который на запрос значения соотв. свойства (dep1 | dep2) либо возвращает уже готовый объект (Object1 | Object2), если он есть в контейнере, либо запускает процесс создания соотв. зависимости (Object1 | Object2).


          Т.е., конструируемый объект ничего не знает о Контейнере. Он предполагает, что зависимости передаются ему упакованными в объект spec. Весь magic с прокси-объектом и созданием объекта я взял у awilix. Я бы и весь awilix взял, только я не увидел, как его можно заюзать в браузере. А мне интересно было затянуть DI в SPA, пришлось брать только самую "мякотку".


          5 и 6 пункты нужно будет помозговать. Пока что просто интересно сравнить "классический" DI (PHP, Java /с .Net не работал/) и JS'овский (с учётом отсутствия типизации параметров функций и возможности минификации кода).


          1. VolCh
            22.08.2019 17:17

            А у вас контейнер только синглтоны позволяет и только через new ?


            1. flancer Автор
              22.08.2019 17:35

              Да.


          1. symbix
            22.08.2019 17:23

            Вот как бы и передается, и не передается — смотря как посмотреть! Если посмотреть в ES5 после транспайлера — наверняка получится, что передается. :-) В любом случае, тут зависимость от контейнера, хоть и неявная: класс сам определяет, что именно взять из контейнера (а что это красиво завернуто в синтаксический сахарок — это принципиально ведь не меняет ничего).


            Можно представить себе такое использование DI, в котором одни синглтоны и все зависимости определены по конкретному классу (без абстрактных классов или интерфейсов) — вот будет примерно оно.


            Что касается типизации — я мыслю Typescript-ом :-) И тут, кстати, обнаружил, что его особенность (type erasure) прекрасно ложится на старое и уже позабытое понимание отличий абстрактного класса (включая pure) от интерфейса. В старых книжках начала 90-х все было строго: (абстрактный) класс — это is-a (скажем, Logger), а интерфейс — это can (скажем, Countable или Comparable). Исходя из этого, получается, что зависимости логично делать как раз от [pure] abstract class, и тут как раз никакой type erasure не мешает (и все эти трюки с символами совершенно ни к чему).


            В голом JS — ну, пусть даже токеном будет не абстрактный класс, а строка (которая ключик объекта), но как понять, чей именно конструктор запросил контейнер (вот она, сервислокаторная сущность вылезла: в DI-то по определению известно!)?


            1. symbix
              22.08.2019 17:31

              А, вот еще мысль.


              Как у вас выглядит самый первый вызов, во входной точке?


              Если new SomeClass(container) — так вот он, сервис-локатор самый что ни на есть.
              А если container.make(SomeClass) — то, кажется, все вполне решаемо, если завести стек — и вроде прям всамделишный DI получится.


              1. flancer Автор
                22.08.2019 17:38

                import Container from "./src/Container.mjs";
                const container = new Container();
                container.addSourceMapping("Vendor_Module", "../example");
                container.get("Vendor_Module_App")
                    .then((app) => {
                        app.run();
                    });


                1. symbix
                  22.08.2019 17:42

                  Ну, то есть если для упрощения откинуть асинхронщину, то будет container.get(App).run().
                  Вроде все решаемо!


            1. flancer Автор
              22.08.2019 17:41

              Если посмотреть в ES5 после транспайлера — наверняка получится, что передается. :-)

              Здесь нет транспиляции. Это чистый JS.


              1. symbix
                22.08.2019 17:43

                Ну, может, я IE захотел. :-) Ок, плохой пример, давайте посмотрим в опкоды V8. :-)


          1. justboris
            22.08.2019 19:31

            Но в том-то и дело, что сам контейнер в классы не передаётся. Т.е., нет такого: constructor(container: Container) spec — это примитивный объект,

            Я открыл исходник и вижу Proxy вместо примитивного объекта. То есть, у вас все-таки получается container, замаскированный через Proxy. Работать будет, но тем не менее.


            P.S. с уровнем вложенности в этом коде просто беда… Что помешало использовать async-функции?


            1. flancer Автор
              22.08.2019 20:03

              В принципе можно сделать парсинг аргументов конструктора такого вида:


              constructor({Vendor_Module_Config, Vendor_Module_Service}) {...}

              и обойтись без прокси. Это будет true DI в таком случае?


              const deps = get_dependencies(Type.constructor);
              const spec = {};
              for(const dep of deps) {
                  spec[dep] = _container.get(dep);
              }
              const result = new Type(spec);

              Суть DI не в том, как я вставляю зависимости в объект, а в том, что сам объект ничего не знает, как я вставляю зависимости в него.


              с уровнем вложенности в этом коде просто беда… Что помешало использовать async-функции?

              Я пока ещё не мыслю asynchronously — у меня очень массивный Java/PHP background синхронного программирования. Плюс замыкания — я там тоже не силён. Сделал так, как оно работало.


              1. justboris
                22.08.2019 20:06

                Не обязательно что-то переделывать, нужно просто называть вещи своими именами.


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


              1. justboris
                22.08.2019 20:09

                Я пока ещё не мыслю asynchronously

                Здесь не нужно как-то по другому мыслить. Был такой код


                import(src).then((module) => {
                   const Type = module.default;
                   // и т.д.
                })

                стал такой


                const module = await import(src);
                const Type = module.default;
                // и т.д.

                Получается компактнее и читаемее.


                1. flancer Автор
                  22.08.2019 20:25

                  Не получается :(
                  image


                  1. symbix
                    22.08.2019 20:32

                    Ну вот конкретно для Ноды я вообще не понимаю, зачем заморачиваться с асинхронщиной для модулей. (Для браузера — понятно)


                  1. justboris
                    22.08.2019 20:35

                    Правильно, нужно саму функцию отметить как async


                    async function create_object(id) {
                       // ... code ...
                    }

                    Проблема в том, что у вас там еще пара замыканий на пути, и их тоже нужно переделать… Но миграция на async-функции стоит того. Вот материал на эту тему с самым большим количеством плюсов: https://habr.com/en/company/ruvds/blog/326074/


    1. flancer Автор
      22.08.2019 10:37

      Это из wiki:


      Внедрение зависимости (англ. Dependency injection, DI) — процесс предоставления внешней зависимости программному компоненту. Является специфичной формой «инверсии управления» (англ. Inversion of control, IoC), когда она применяется к управлению зависимостями. В полном соответствии с принципом единственной ответственности объект отдаёт заботу о построении требуемых ему зависимостей внешнему, специально предназначенному для этого общему механизму[1].

      Как ниже заметил коллега risedphantom :


      DI же в двух словах — вместо require/import модуля вы инжектируете зависимость через параметр конструктора (или сеттер свойства). То есть за этим громким словом стоит простое "передавайте зависимости класса через параметры конструктора".

      По-простому если, то Service Locator самый что ни на есть true DI, если он при создании объектов "передаёт зависимости через параметры конструктора". "Мокрое" вполне может быть одновременно и "зелёным".


      Коллега VolCh вообще считает, что DI'ность контейнера не "врождённое" свойство, а зависит от того, как мы его используем. Один и тот же контейнер может одними разработчиками использоваться как DI-контейнер, а другими — как "не-DI, но тоже очень хороший", контейнер.


  1. ReklatsMasters
    22.08.2019 01:52
    +2

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


    Кстати, от mjs отказались, теперь в package.json просто будет указывается type: module.


    1. VolCh
      22.08.2019 06:37
      +1

      А как вы в модули инжектите их зависимости с возможностью их переопределения не трогая кода модуля?


      1. ReklatsMasters
        22.08.2019 09:23

        jest на этапе тестирования это умеет из коробки.


        1. flancer Автор
          22.08.2019 10:20

          Думаю, что такое возможно, если jest берёт на себя функцию загрузки модулей и подменяет модули моками на лету. Вот только когда я пытался создать тестовое окружение для разработки ES-модуля при помощи jest пришлось перейти на mocha — там это делается гораздо проще. Я не говорю, что jest не работает с ESM, я не изучал этот вопрос. Я лишь говорю, что подружить mocha с ESM гораздо проще, чем jest. Возможно, что как раз из-за этого умения.


        1. VolCh
          22.08.2019 12:55

          А на продакшене в рантайм тоже jest тащите?


    1. flancer Автор
      22.08.2019 06:45

      ES-модули — это прежде всего код, исходники. А DI — это окружение (рабочее, девелоперское, тестовое). Контейнер содержит уже настроенные для функционирования объекты, связанные между собой тем или иным образом. В языках с интерфейсами можно подменять их имплементации в Контейнере на уровне конфигурации Контейнера.


      Я использовал DI в Java и PHP, а там нет es-модулей. Так что основной ответ на вопрос "зачем" — мне так удобнее :)


      Кстати, от mjs отказались, теперь в package.json просто будет указывается type: module.

      Это работает, начиная с v12, 11-я версия ноды всё ещё требует наличия *.mjs даже с type:module.


    1. symbix
      22.08.2019 16:06
      +1

      Последняя буковка в SOLID.


      1. VolCh
        22.08.2019 17:18

        DI который Dependency Injection — это про первую букву в SOLID :)


  1. risedphantom
    22.08.2019 09:13
    +1

    Zanuda mod.on


    "Инверсия управления (англ. Inversion of Control, IoC) — важный принцип объектно-ориентированного программирования, используемый для уменьшения зацепления в компьютерных программах. Одной из реализаций инверсии управления в применении к управлению зависимостями является внедрение зависимостей (англ. dependency injection). Внедрение зависимости используется во многих фреймворках, которые называются IoC-контейнерами." ©


    DI же в двух словах — вместо require/import модуля вы инжектируете зависимость через параметр конструктора (или сеттер свойства). То есть за этим громким словом стоит простое "передавайте зависимости класса через параметры конструктора".


    Zanuda mod.off


  1. sanchezzzhak
    22.08.2019 13:36

    Для серверной разработки di не нужен, у нас есть микросерисы, которые подключают все что нам нужно в рамках сервиса, меня этот расклад устраивает на 100%.


    Для фронта, я бы хотел видить di загрузку кода по требованию без костылей, но и за большого зоопарка андройдов с хромом 45 и ниже версии, мы не можем использовать es6 и выше, только через babel (приходится делать 2 версии)


    на Type script ecть http://inversify.io/ зависимости подключают через декоратор, что очень симпатично.


    1. funca
      24.08.2019 15:42

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


  1. AlexSpaizNet
    22.08.2019 15:02
    +1

    Возможно это только мой опыт, но когда перешел в мир жаваскрипта из PHP/Python/Java/Go, очень тяжело объяснять разработчикам зачем нужен DI (я не имею ввиду фреймворк, я имею ввиду чистый ручно инжекшен зависимостей). Ну привыкли они делать require() и все работает. Тесты? Привет rewire, proxyquire и другим костылям… которые тоже не со всем справляются когда весь проект выглядит как клубок из require(), имеются cycle dependencies и код выполняется прямо после вызова require(), а не когда Я как разработчик захочу…

    А когда количество тестов растет и они начинают падать непонятно почему (понятно, где то что то замокалось один раз и все, потому что референс на функцию зарезолвился при require(), потому что никто не понимает что тесты бегут в одном процессе...), когда что бы протестировать хоть что то нужно делать танцы с бубном… я начинаю плакать Ж(

    Как же все таки приятно работать когда я могу заинжектить то что мне надо, даже не используя сторонних библиотек…

    У меня есть теория, что если человеку не приходилось заниматься сексом с огромным количеством тестов, ему трудно понять преимущества этого подхода…


  1. GerasimGerasimov
    22.08.2019 17:19

    Не по теме, но чтобы на фронте и сервере использовать CommonJS модули и продвинутые ООП возможности, я по совету старших перешёл на TypeScript


    1. flancer Автор
      22.08.2019 17:40

      Мне на TS перейти религия не позволяет — я транспиляторы не люблю :)


      1. symbix
        22.08.2019 20:34

        После того, как TS мне через секунду после написания кода нашел ошибку (отсутствие обязательного ключа в большом таком объекте) в цепочке из этак 10 rxjs-операторов, я не понимаю, как можно это не любить. Вручную я бы это дебажил полчаса точно — да и не факт, что вообще заметил бы! (Тестами там покрыть затруднительно, обвязка вокруг двух сильно навороченных сторонних API).