Как стать автором
Обновить

Комментарии 40

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

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

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

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


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

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


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

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


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}].`);
        }
    }
}

Мне кажется решение с деструктуризацией параметров в конструкторе — очень изящным решением.


Кстати, можно использовать расширенную реструктуризацию и будет похоже на "тип: переменная" (хоть и наоборот от принятой в TS):


export default class Vendor_Module_App {
    constructor({
        Vendor_Module_Config : config,
        Vendor_Module_Service: service
    }) {
        this.config  = config;
        this.service = service;
    }
}

Изящность (я тут без иронии) вашего варианта в том, что вы "взломали" мой стандартный способ объяснения, чем 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 (в конце концов, даже при правильном использовании как минимум один раз — во входной точке — он именно так и используется). Но не наоборот.

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


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'овский (с учётом отсутствия типизации параметров функций и возможности минификации кода).

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

Да.

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


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


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


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

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


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


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

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

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

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

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

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

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

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


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

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


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 синхронного программирования. Плюс замыкания — я там тоже не силён. Сделал так, как оно работало.

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


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

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

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


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

стал такой


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

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

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

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

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


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

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

Это из wiki:


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

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


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

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


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

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

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

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

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


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


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

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

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

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

Zanuda mod.on


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


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


Zanuda mod.off

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


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


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

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

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

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

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

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

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

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

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

Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории