Pull to refresh

Comments 25

Да по той же причине по которой его использует Nest.js или Angular. У современных приложений очень высокая сложность. По мере роста проекта сервисы начинают зависеть от репозиториев, логгеров, клиентов API, контролировать все связи становится сложно, а при необходимости сменить один логгер на другой можно испытать сильную боль. В этом JS не отличается от других языков. JS и Node.js давно перестали быть просто скриптовыми языками.

а при необходимости сменить один логгер на другой можно испытать сильную боль

На самом деле ваш пример показывает инверсию зависимостей, которую на масштабе решает DI контейнер.

ваш пример показывает инверсию зависимостей

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

Нужен. Очень не хватает. Конечно, все себе по разному представляют js, но если отбросить предрассудки типа «корочки красят в своих интерфейсах», то вполне понятно зачем. JS такой же ЯП как и многие другие, с такими же примерно проблемами, на нем решают примерно такие же задачи и т.д.

Зачем? Примеры кода можете? Чтобы отчетливо было видно что DI-контейнер это прям революция?

DI-контейнер - это никакая не революция. Им сто лет в обед. Я со Spring'ом работал в 2010-х. Это уже была 3-я версия платформы. Затем я использовал DI в PHP - в Magento. Очень своеобразный DI, особенно в первой версии (2008 - 2015 год). Использовал DI из Symfony 2. В PHP вообще с этим делом всё хорошо, даже стандарт есть - PSR-11.

Когда окончательно перешёл в JS даже "запилил" свой DI - чтобы писать в привычной для себя манере. Правда я позволил ИИ его "под себя" переписать (под "силиконовых", а не под "кожаных").

Что касается примеров кода, то могу предложить посмотреть вот этот код - @flancer32/teq-cms. Я писал его "ручками", ещё на "рукописном" DI. А код в @flancer32/teq-web уже написан ИИ (Codex) с "его" DI. Я в код заглядывал, но руками старался не править ничего. Самый, что ни на есть "машинный код".

Рабочий код на фронте - вот. Зайдите в DevTools и посмотрите, что видит браузер:

То же самое, что видит разработчик в IDE
То же самое, что видит разработчик в IDE

Это старый код, но сделан тоже в той же IoC-парадигме с моим рукописным DI в основе. Для построения UI используется vue и Quasar.

Если вы посмотрите все es6-модули, то там не будет ни одного (!) статического импорта. Статические импорты нужны в головном скрипте для загрузки DI-библиотеки, а дальше контейнер сам динамически (через import()) загружает нужные зависимости по мере необходимости. Как следствие, код становится изоморфным (без изменений может использоваться как в браузере, так и в nodejs). Вот вообще без изменений! Поначалу я на бэке импортировал node'овские библиотеки, но сейчас и они инжектятся. То есть, можно и бэкендовый код в браузере запускать, только node-библиотеки нужно замокать (делается через препроцессор контейнера). А сам код приложения остаётся без изменений.

И в отличие от многих других DI-контейнеров (включая контейнер автора статьи), у моего контейнера ограничена возможность что-то зарегистрировать специально:

const container = new Container()
  .registerValue(ID_GENERATOR, randomUUID)
  .registerValue(CLOCK, () => new Date())
  .registerClass(CartApplicationService);

У меня регистрировать можно только если перевести контейнер в тестовый режим:

container.enableTestMode();
container.register("App_Service$", mockService);

То есть, в prod-режиме все es6-модули резолвятся и подгружаются автоматически, сколько бы их там ни было. Нужно только "потянуть" головной модуль, а дальше строится дерево зависимостей и подтягиваются исходники (можно прямо с unpkg.com тянуть). Более того, с учётом наличия в контейнере препроцессора, можно менять маппинг идентификаторов зависимостей на исходный код и реализовывать такие посторонние для "ванильного" JS вещи, как интерфейсы. А с учётом наличия там же постпроцессора, можно использовать в JS и AOP.

Все эти приёмы и технологии давным давно используются в программировании и никакая не революция. Просто они используются там, где кодовая база проекта начинает заваливать за миллионы строк кода (в Magento 2, например, больше 2 млн. и для работы с JS используется requirejs, тоже своего рода DI).

Много у вас примеров таких объёмных кодовых баз именно на JavaScript? А для кодирования на одной веб-страничке чего угодно такие навороты действительно не нужны.

Spring - это понятно. Это Java и приколоченное гвоздями ООП. А в Javascript то зачем? В Javascript классы это просто "синтаксический сахар". Зачем вообще тащить в программирование на Javascript ООП шаблоны? В нем без них значительно проще, быстрее и дешевле.

Javascript классы это просто "синтаксический сахар"

Предрассудки...

class Bar {
  method1() {
    // ...
  }

  method2() {
    // ...
  }
}

class Foo extends Bar {
  
  // Static initialization block
  static {
    try {
      
    } catch {
      
    }
  }

  // Private static field
  static #field = 1;

  // Private static method
  static #method() {}

  // Private static getter
  static get #foo() {
    return this.#privateField;
  }

  // override parent method
  method1() {
    // ...
  }

  // extend parent method
  method2() {
    super.method2()
    // ...
  }

  // child private method
  #method() {}
}

Подскажите, это «синтаксический сахар» над чем?

Подскажите, это «синтаксический сахар» над чем?

Над механизмом прототипов

Built on, не значит «сахар». Вы выделили то, что вам понравилось, но ведь сразу за выделенным текстом идет: but have some syntax and semantics that are unique to classes.

хаха, обожаю такой черрипикинг, где скрин опровергает самого человека, что-то доказывающего)

Вы приявязыветесь к языку, паттерны же решают какой-то класс задач и language-agnostic. Так например signals, reactor, observable, тот же di, они сейчас в разных библиотеках и вы ими неосознанно пользуетесь каждый день. На счёт классов, они стали не совсем синтаксическим сахаром, сейчас есть небольшие различия во внутренней реализации, но это тоже не важно. Прототипы и наследование через них просто один из вариантов реализации ООП. Мне лично нравится объяснение дядюшки Боба про различия разных парадигм.

Все! Я понял. Вы не можете без ООП. А Javascript прекрасен как раз тем, что на нем можно быстро, дешево и качественно делать код без ООП. Поэтому я на него и пересел после C++, потом Java, потом C#, потом Python...

Мне нравится использовать мультипарадигменныц подход, для описания доменной части ООП очень удобен, а так пишите конечно как нравится 😅

DI - это про внедрение зависимостей. Зависимости есть в любом коде, состоящем из более, чем одного файла. Статические импорты в JS - это тоже зависимости.

DI в JS можно вообще без классов использовать:

// src/App/Config.mjs

export default () => ({
    greeting: 'Hello',
    punctuation: '!',
});
// src/App/FormatName.mjs

export default () => (name) => {
    const value = name.trim().toLowerCase();
    return value.charAt(0).toUpperCase() + value.slice(1);
};
// src/App/Greet.mjs

export const __deps__ = {
    default: {
        config: 'App_Config$',
        formatName: 'App_FormatName$',
    },
};

export default ({config, formatName}) =>
    (name) => `${config.greeting}, ${formatName(name)}${config.punctuation}`;
// bootstrap.mjs

import Container from '@teqfw/di';

const container = new Container();

container.addNamespaceRoot(
    'App_',
    new URL('./src/App/', import.meta.url).href,
    '.mjs'
);

const greet = await container.get('App_Greet$');

console.log(greet('  aLEX  '));

Вот архив с кодом - достаточно распаковать, затем npm install & npm start.

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

Можно вас попросить привести нормальный пример, когда просто импорта зависимости не хватает и вам нужен именно DI? Какую проблему вы решаете с помощью DI?

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

Дело не в правоте, а в возможности решать задачи наиболее удобным для вас путём. Если вы не видите, где применять JS-код без статических импортов или использовать позднее связывание - вам просто это не нужно. Было бы нужно, вы бы зацепились глазом за пример выше и вас бы сразу триггернуло. Я вас уверяю, это и есть нормальный пример. Там всё видно. По крайней мере тем, кому это надо или может быть надо.

Какую проблему вы решаете с помощью DI?

Лично я решаю с помощью DI проблему "распределённой разработки кода слабосвязанными командами разработчиков". Я очень быстро забываю контекст проекта. Через полгода уже не помню ничего, если в течение этого времени не заглядывал код. Так что "я сейчас" и "я через год" - это и есть слабосвязанные команды разработчиков :)

А в принципе, весь IoC для этого был придуман. DI - лишь один из вариантов.

Как только у вас появляется container.register, это уже service locator. Вообще, все попытки реализовать тру di в js в конечном итоге сводятся к созданию локатора.

В вашем решении меня смущают анонимные классы, я привык к instanceof.

Передача параметров объектом тоже смущает. Аргументы в пользу — убедительные, но это софистика. Дело в том, что если мне нужен один-два аргумента, я не запутаюсь, а если 10, то стоит пересмотреть этот класс, с ним явно что-то не так.

Но попытка интересная.

С утверждением про register() я не до конца согласен. Само наличие реестра и метода resolve() еще не делает решение Service Locator. Для меня граница проходит в месте использования контейнера. Если контейнер передается внутрь прикладного кода и класс сам вызывает resolve(), зависимости действительно становятся скрытыми и получается локатор. В примере контейнер остается в composition root, а сервис получает конкретный набор зависимостей через конструктор и вообще не знает о существовании контейнера. Собственно для меня это самое важное отличие - про локатор знают все.

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

Классы в примере анонимные только в выражении, из которого они создаются. component() создает класс один раз, задает ему имя и возвращает стабильную ссылку. Поэтому обычный instanceof работает:

const service = container.resolve(CartApplicationService);

service instanceof CartApplicationService; // true

Новый класс при каждом resolve() не создается.

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

DI выглядит как антипаттерн. Волосы стынут в жилах, когда в рантайме решается, взлетит приложуха или нет

У нас в проде NestJS, там reflect-metadata и decorators, и один раз получили сюрприз: перевели сборку на esbuild ради скорости, а decorators metadata сломались, потому что esbuild эмитит их не так как tsc. Две недели дебага, прежде чем поняли что дело не в коде. Ваш подход с explicit inject через обычные объекты от этого класса проблем избавляет полностью, ценой ручного описания зависимостей.

Sign up to leave a comment.

Articles