Обновить

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

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

"Ручной менеджмент токенов" - это лишь один из возможных вариантов управления зависимостями. Я, например, и мои агенты, пишем так (ну, или примерно так):

export default function Service({ repository }) {
  return {
    async getProfile(id) {
      const name = await repository.findNameById(id);
      return { id, name };
    },
  };
}

export const __deps__ = {
  repository: "App_User_Repository$",
};

При отсутствующей runtime-рефлексии в JS приходится "колхозить" runtime-рефлексию самому. Зависимости всех доступных снаружи экспортов отдельного es6-модуля прописываются в специальном экспорте __deps__. После этого анализ зависимостей модуля можно автоматизировать полностью. Синтаксис "языка токенов" может быть любым, я взял для себя namespace'ы из PHP Zend1 фреймворка. Главное, чтобы этот "язык" упаковывал в токене:

  • адрес npm-пакета и es6-модуля в нём;

  • имя экспорта в es6-модуле;

  • форму существования зависимости (singleton or transient).

После этого достаточно в настройке контейнера в Composition Root привязать физическое расположение пакета (в локальной файловой системе или в вебе) к соответствующему namespace'у (App_ или App_User_) и разрешение зависимостей из __deps__ происходит автоматически, как в C#, Java, PHP, ...

Решение с __deps__, кстати, мне ИИ подсказал. Это логика "силиконовых", а не "кожаных". Им так норм.

Это очень сомнительное решение, с какой бы стороны я не пытался на него смотреть

Что именно вас смущает в этом коде?

export default function Service({ repository }) {
  return {
    async getProfile(id) {
      const name = await repository.findNameById(id);
      return { id, name };
    },
  };
}

Фабрика создаёт объект и инжектит в него зависимость. Обычный JS-код. Можете переписать его под классы, если вам больше нравятся классы.

Странно что вас ничего не смущает.

Откуда у вас уверенность что repository имеет функцию findNameById, которую вы вызываете в методе?

А если нет, то - что?

Интерфейс - это описание ожиданий компонента на границе с его зависимостью.

Компонент ожидает, что "repository имеет функцию findNameById" - это его право. В данном случае ожидания выражены через код. Можете добавить JSDoc'и, если хотите подробностей. Мы сейчас про JS говорим, тут - вот так. В других языках контракты (ожидания) описываются по-другому.

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

Реально в js нет интерфейсов... ну это надо как-то проверять тогда в коде чтоли...

мда, жесть конечно, язык как будто для маленьких манипуляций с html сделали, а не для серьёзных проектов...

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

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

ну это надо как-то проверять тогда в коде чтоли...

Проверять не надо - неэффективно. Достаточно обеспечить поставку зависимости с соответствующим контрактом и обработку ошибок на нужном уровне приложения.

язык как будто для маленьких манипуляций с html сделали

Так оно и было. Всё остальное наросло сильно позже.

Я начинал программировать на PHP, когда там ещё не было возможности указывать типы в сигнатуре функции (до PHP 5.0, 2004 год):

function process($user) {}

Очень похоже на нынешний JS, не правда ли?

После 5.0 стало можно писать так:

function process(User $user) {}

А после PHP 7.0 (2015) так:

function sum(int $a, int $b): int {}

Я уверен, что если бы не Microsoft с TypeScript, то и в JS уже давно была бы такая возможность. Но имеем, что имеем. В JS в 2026-м году нет ни интерфейсов, ни возможности указать типы в сигнатуре функций.

Поэтому вот - описываем контракты в коде. Как в PHP четвертьвековой давности.

Поэтому вот - описываем контракты в коде.

Я вам и пытаюсь сказать что нету никакого контракта у вас.

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

Даже наследование класса и переопределение метода выглядит лучше чем "просто знать" где там что вызывается

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

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

В TS есть возможность описать interface в отдельном исходнике, который при транспиляции не превращается в js-код, из-за того, что в JS нет интерфейсов вообще.

Зато в JS есть JSDoc и возможность описать интерфейс как в отдельном файле, так и в types.d.ts (если в пакете интерфейсов не много).

Я в примере обращал ваше внимание, что интерфейс зарождается в коде потребителя. Это справедливо для любого ЯП.

"Интерфейс репозитория" вырастает не из "общих свойств всех репозиториев", а из "общих ожиданий всех потребителей всех репозиториев".

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

А так вам просто нужно место (файл), где описать какой-то интерфейс в том или ином виде, предусмотренным соответствующим ЯП, или в md- или txt-файле, если этот ЯП ещё менее развит, чем JS с его JSDoc.

В языке где есть интерфейс достаточно написать implements (например php) и быть уверенным что ваши репозитории поддерживают контракт.

Я в примере обращал ваше внимание, что интерфейс зарождается в коде потребителя. Это справедливо для любого ЯП

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

А так вам просто нужно место (файл), где описать какой-то интерфейс в том или ином виде, предусмотренным соответствующим ЯП, или в md- или txt-файле, если этот ЯП ещё менее развит, чем JS с его JSDoc

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

В языке где есть интерфейс достаточно написать implements (например php) и быть уверенным что ваши репозитории поддерживают контракт.

Да. Но это в тех языках, где "интерфейс" поддерживается на уровне языка. В JS такого нет.

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

Именно так. Вы исходите из нужд конечного пользователя приложения (потребителя) и выстраиваете весь домен вокруг этих нужд, пытаясь их "угадать" / "вычислить" / "опросить". В коде точно так же. Успешность вашего интерфейса зависит от того, насколько точно вы представляете нужды вашего потребителя.

Сидеть и сверять программу с текстом, когда за выполнением контрактов должен отвечать компилятор/интерпретатор это явно два шага назад

Полностью согласен с такой постановкой вопроса. Не человек должен сверять код с кодом, а специальные программы. Поэтому, когда я перешёл с PhpStorm на VSCode, у меня просто волосы дыбом встали от того, насколько плохо поддерживается JSDoc в VSCode. В IDEA (WebStorm, PhpStorm, ...) IDE самостоятельно связывает JSDoc-аннотации @interface и @implements в JS-коде и обеспечивает навигацию по коду, его проверку и автокомплит. И WebStorm умел это делать ещё тогда, когда TypeScirpt'а ещё не существовало.

Чтобы получить аналогичное поведение в VSCode, нужно постараться. Но это и понятно, Microsoft изо всех сил развивает свои активы (TypeScript и VSCode). tsserver не будет поддерживать JSDoc в полной мере.

Именно так. Вы исходите из нужд конечного пользователя приложения (потребителя) и выстраиваете весь домен вокруг этих нужд

Понял, вопросов больше не имею

А вы попробуйте обсудить эту точку зрения с вашим системным аналитиком :)

Системный аналитик должен знать что такое инверсия зависимости?) Или, например, почему в луковичной архитектуре стрелочки зависимостей рисуют к домену? То есть более низкоуровневый код должен зависить от более высокого?) Ну это, как будто, должны программисты знать) Почему домен не должен зависить от базы данных))

Хотя мне в комментариях один тимлид ВК высказывал что им никакая инверсия не нужна и это все кодовое графоманство) Ну я хочу сказать что и приложение ВК не лучший пример для подражания)) Да и акции уже по 130 рублей или сколько там))

Системный аналитик хорошо представляет откуда берутся требования к вашему коду.

Посмотрите, что такое "I" в аббревиатуре "SOLID" (Interface Segregation Principle). Он как раз об этом - требования идут от потребителя, а не от возможностей поставщика.

Ну я тоже хорошо представляю откуда берутся требования.

По вашему разделение интерфейса на более мелкие, функциональные части, это построение инверсии зависимости??

Segregation как раз переводится как разделение, и не как не связанно с направлением требований...

Сформулируйте, пожалуйста, ISP в том виде, который вы используете. Я покажу, что говорит о направлении требований.

Подробно можно прочитать в книге Роберта Мартина "Чистая архитектура"

Вот классическая формулировка Роберта Мартина:

Clients should not be forced to depend upon interfaces that they do not use.

О направлении требований говорит "Clients should not be forced to depend...". Клиенты (потребители) не должны зависить от неиспользуемых ими интерфейсов.

Это значит, что клиенты сами определяют свою зависимость от интерфейсов, которые им нужны (that they use). Независимо ни от кого. И это уже дело среды исполнения обеспечить поставку таких компонентов, которые нужные клиенту интерфейсы имплементируют. При этом реальные возможности этих компонентов потребителя (клиента), за границами его нужд, не волнуют совершенно. Это может быть God Object, может быть "оркестр", совмещающий несколько интерфейсов, а может быть единичная имплементация именно этого интерфейса. As you wish, как говорится.

Требования к интерфейсу задаётся в местах его использования, а не в местах его имплементации.

Ну я согласен что от неиспользуемых интерфейсов зависимость не нужна, это вроде очевидно.

Вы вырвали одну фразу, которая к делу, по сути, даже не относится, из книги где нужно последовательно читать главы чтобы все собрать в одну картину...

Я вас не хочу ни в чем переубедить. Я использую инверсию зависимости в своих проектах и придти к пониманию мне помогла эта книга.

Вы вырвали одну фразу

Ведь это и есть классическая формулировка ISP по Мартину, разве нет?

Я использую инверсию зависимости в своих проектах

Проекты на JS или на TS? Судя по "Реально в js нет интерфейсов... ну это надо как-то проверять тогда в коде чтоли...", не на JS.

А я использую инверсию зависимостей в своих проектах именно на JS. Не на TS, не на Java, не на PHP, а именно на "ванильном" ES6+. И эта инверсия у меня такая, какая есть, именно потому, что это JS, а не TS.

Вы сужаете целые главы книг до одной фразы и спрашиваете весь ли это смысл? Мне кажется очевидно что нет.

Я через день пишу на чистом js фронт, мне на нем не приходилось, пока, строить сложные вещи, но я уверен что если погрузиться в этот вопрос глубже, инверсия зависимости подругому реализуется, возможно напишу про это что-то))

Кто сейчас вообще пишет на чистом js серьезный проект.

Инверсия зависимостей в js реализуется точно также как и везде, di контейнер просто добавляет удобства.

Вы считаете, что DI нет смысла использовать в несерьёзных проектах?

Серьёзность проекта показывает результат, а не библиотеки

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

Если бы тс выполнялся в браузере, без проблем я бы лучше его взял.

А так, покрасить кнопочки, отправить ajax, мне хватает с лихвой. Бизнес логики никакой нет на фронте.

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

но я уверен что если погрузиться в этот вопрос глубже, инверсия зависимости подругому реализуется, возможно напишу про это что-то))

С интересом прочитаю вашу точку зрения. Даже подпишусь, чтоб не пропустить :)

У меня тоже не получается вам объяснить что сократить до одной фразы понятие из книги нельзя)) Не учитывая что и фразу вы приводите не ту))

Спасибо за мотивацию разобраться)

Главное, чтобы этот “язык” упаковывал в токене адрес npm-пакета и es6-модуля в нём; имя экспорта в es6-модуле;

То есть чтобы заменить потом SqliteRepository на PostgresRepository, надо будет либо во всех зависимостях токен заресплейсить, либо поддерживать в Composition Root кастомный маппинг и поди найди потом что вообще сюда инжектится? В typescript хотя бы по имплементации интерфейса можно было бы поискать в один клик что вообще может прийти, а в чистом JS очень уж дикая неявность получается. Силиконовые конечно разберутся, но силиконовые вообще могут ручной Composition Root поддерживать который хотя бы более явный будет.

То есть чтобы заменить потом SqliteRepository на PostgresRepository, надо будет либо во всех зависимостях токен заресплейсить, либо поддерживать в Composition Root кастомный маппинг

Совершенно верно. Но и для TypeScript это точно также нужно будет искать и менять. И для любого друго ЯП тоже. Если вы замкнулись на конкретную имплементацию, а не на интерфейс.

Но вы можете сделать "фейковый интерфейс" в JS (вот работающий пример):

/** @interface */
class IRepository {}

и две имплементации:

/** @implements {IRepository} **/
export default class SqliteRepository {}

и

/** @implements {IRepository} **/
export default class PostgresRepository {}

А в зависимостях компонентов указывать именно интерфейс

export default function Service({ repository }) {}

export const __deps__ = {
  repository: "IRepository$",
};

Всё, как в других ЯП.

Да, в Composition Root нужно будет решать, какая из имплементаций интерфейса в какой компонент инжектится ("везде одна и та же" или "в эти - одна, в те - другая"). Но такое решение нужно принимать везде, где указан интерфейс вместо имплементации. Я ж говорю, всё, как в других ЯП.

DI делает зависимость явной и переносит выбор имплементации за пределы компонента. Это позволяет отдельно проверять потребителей и реализации и тем самым удерживать стоимость верификации под контролем.

Это аргумент про фабрику, скорее. А фабрика, в свою очередь, является реализацией DI на языке, в сочетании с замыканиями. И без всяких фреймворков. Про "удержание стоимости верификации под контролем" было вообще больно читать.

Ага, учитывая что тип можно указать в сигнатуре функции и зависимость тоже будет явная, без всяких DI...

DI это про контейнер который содержит объекты для инъекции, а не про инверсию зависимостей...

А фабрика, в свою очередь, является реализацией DI на языке, в сочетании с замыканиями. И без всяких фреймворков.

Можно и так - вручную. Фреймворки лишь помогают унифицировать общие подходы в проекте. Или в проектах. Или в отдельной экосистеме. Всё зависит от масштаба.

Про "удержание стоимости верификации под контролем" было вообще больно читать.

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

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

"Программирование - безнадежно проигранная борьба с непреодолимой сложностью кода и вероломством требований" (с) Jonathan Edwards

Согласен, что фраза "удержание чего бы то ни было под контролем" в программировании звучит излишне оптимистично. DI помогает проиграть эту борьбу чуточку позже.

И в итоге тесты зелёные, а на проде не работает, каеф

Так бывает и без DI. Вероятно, вы проверяете то, что не нужно, а то, что нужно, не проверяете. Делайте наоборот.

это всё спокойно делается без функций обёрток, очередная вода на тему психических отклонений

"Это всё" слишком широкое понятие. А излишняя самоуверенность есть признак узкого кругозора.

"Это всё" очевидно в границах статьи. Про scope creep слышали или к реальным проектам DI-феласафов таки перестали пускать?

Пример приведете как это сделать?

дайте мне ваш содержательный и сжатый вариант до и после DI -- я приведу вариант без DI

Не сомневаюсь. Дело не в этом. Смысл DI - в эволюции кода. Одноразовые программы можно писать сразу в машкодах, если у вас к ним способности.

Кто?

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

Эта статья как будто должна отвечать на мой вопрос, но в очередной раз приводит одну единственную проблему, решаемую с помощью DI - тестирование. Поэтому в очередной раз должен задать стандартный вопрос: чем вам не угодил манкипатчинг для тестирования? Используйте мокинг, чтобы подменять ./storage.js и не придётся усложнять продуктовый код.

Используйте мокинг, чтобы подменять ./storage.js и не придётся усложнять продуктовый код.

Покажите, пожалуйста, код, как это делается. Я пришёл в JS из мира Java & PHP. Возможно, я просто не знаю каких-то базовых основ или знаю их по-другому.

в очередной раз приводит одну единственную проблему, решаемую с помощью DI - тестирование

Я использую интерфейсы в своём JS-коде без транспиляции из TS (или любого другого ЯП) путём подмены токена интерфейса токеном имплементации в настройках DI-контейнера (препроцессор). Как вы используете интерфейсы в JS?

Я использую Proxy для добавления cross-cutting функциональности в свои приложения на JS. Через post-процессор я могу обернуть любой объект, который используется в качестве зависимости, включая библиотеки nodejs. Как вы это делаете в JS-коде без транспиляции из TS (или любого другого ЯП)?

const saved = [];

vi.mock('./storage.js', () => ({
  async save(value) {
    saved.push(value);
  },
}));

it('saves normalized value', () => {
  await saveName(' Alex ');
  expect(saved).toEqual(['alex']);
});

https://vitest.dev/guide/mocking

Поэтому и говорят, что в JavaScript DI не нужен. Хотя лучше сказать, что он нигде не нужен. К тестам совершенно не такие требования, как к продуктовому коду. Вполне уже можно было изобрести какую-то магию с подменами и для Java. Но вместо этого джависты коверкают и тащат магию в продуктовый код.

Это не monkey patching :) Вы сейчас притянули целую библиотеку - vitest, у которой более 20 зависимостей. Вполне возможно, что "под капотом" там используется monkey patching, но вот вы сами - используете библиотеку. Так же, как и я. Только я использую свою библиотеку teqfw/di (у которой 0 зависимостей) для сбор прода, а вы используете vitest (с 20 прямыми зависимостями) для тестов.

Для исходного кода в DI-стиле:

export function createSaveName({storage}) {
  return async function saveName(name) {
    const value = name.trim().toLowerCase();
    await storage.save(value);
    return value;
  };
}

Тестовый код выглядит так:

import assert from "node:assert/strict";
import test from "node:test";
import {createSaveName} from "./save-name.mjs";

test("saveName", async () => {
  let saved;

  const saveName = createSaveName({
    storage: {save: async (value) => saved = value},
  });

  assert.equal(await saveName("  Alex  "), "alex");
  assert.equal(saved, "alex");
});

Без каких-либо фреймворков - простой JS на проде, простой JS в тестах. Всё в рамках обычного nodejs. Это - DI в самом его рафинированном виде.

Вы можете таким образом весь проект разбить на es6-модули без единого импорта, указывая зависимости в аргументах фабричной функции (конструктора), а затем в одном головном файле статически импортировать все модули и вручную собрать тот граф зависимостей, который вам нужен. Так никто не делает, но можно.

Своим vitest вы просто обернули и скрыли сложность тестирования. Что будет, если сделать import ./storage.js до того, как сделать vi.mock('./storage.js'...)? Например, в setupFiles? Статические импорты - они такие.

И вы всё ещё пока не ответили про интерфейсы и cross-cutting функциональность. Про интерфейсы я за вас отвечу - вы их не используете, потому что в JavaScript интерфейсов нет. И cross-cutting вы тоже не используете, т.к. у вас нет единой точки создания объектов, где вы могли бы их модифицировать (в TS есть транспиляция и аннотации, а в JS - нет). А ещё у меня из контейнера все объекты выходят “замороженными” и monkey patching на них не работает. Вам даже не понять, для чего нужна может быть такая функциональность. Вы из другого мира - не из мира Java/PHP.

Проблема в том, что Java и PHP в браузере не работают - только JS. Вот поэтому я, как джавист, “коверкаю и тащу магию в продуктовый код” :) Мне удобнее писать JS-код так. Мне и моим ИИ-агентам.

Смотрите, а вот вам пример который я нашел, без всякой ИИшницы, как думаете, можно на нем построить что-то типо интерфейса?) Еще и вот таким шлифануть)

По сути, instanceof выносит сигнатуру в тело функции)) что, наверное, просто минус языка js как мультипарадигменного, попытка уседеть на скольки-то стульях))

Можно попытаться, но получится плохо. На прототипах строятся, скорее, абстрактные классы, а не интерфейсы. Вот тут прочитайте про интерфейсы в разделе "2. Первая ось: универсалии, или где живёт общее". Текст не мой, но очень интересный. Он объясняет, почему после стольких лет доминирования наследования в ООП появилась композиция.

Проблемы отличия абстрактного класа от интерфейса возникают при множественном наследовании. На малых кодовых базах их не будет. А в Magento были и я их лично щупал - как совместить в одной кодовой базе 3-4 плагина сторонних разработчиков, которые друг про друга не знают, но которые по-своему переопределяют один и тот же базовый функционал платформы? Причём в Magento для этого и инструменты были, и даже переопределялось, и даже работало в проде потом.

Но если вы сделаете DI на прототипах, я с интересом ознакомлюсь.

Кстати, я тут подумал, что я в своём JS-коде сейчас вообще нигде не использую `extends`. У меня как раз композиция вместо наследования.

Мне удобнее писать JS-код так.

В этом и проблема. Вы смотрите на проблему сквозь ваши привычки, и не пытаетесь посмотреть на неё объективно. Попробую ещё раз заострить ваше внимание:

  • Между изначальным saveName с явным импортом и createSaveName драмматическая разница в синтаксической сложности. saveName - это первый урок по программированию для детей. createSaveName использует более сложные концепции. Вы сделали продуктовый код сложнее просто по конструкции.

  • Ясно, что эту конструкцию вы привыкли быстро считывать. Но она сложнее не только по написанию. Она из явного импорта, явного указания на конкретную зависимость, с которой работает saveName, делает неявную. Для каждой такой зависимости сразу возникает вопрос: а какие имплементации интерфейса вообще существуют и могут сюда приходить? Предвидя ваше возражение, сражу скажу, что на связность кода это никак не влияет. Сама по себе возможность подменить сторадж никак не делает слабее связность между saveName и ./storage.js . Если программист в логике гвоздями приколотил одно к другому (что не обязательно плохо), то вся эта инверсии контроля - просто обман читателя кода.

  • Усложнение навигации по коду - то ещё удовольствие. Вместо того, чтобы сразу перейти к зависимости, вам нужно прыгнуть на интерфейс, а с него на продовую реализацию.

  • Тест с манкипатчингом (да, vitest, jest, sinon, jasmine - все тестовые фреймворки используют манкипатчинг уже точно больше 10 лет), напротив, более явно выражает, какие зависимости подменяются в тесте. Но здесь разница не столь критична.

И вы всё ещё пока не ответили про интерфейсы и cross-cutting функциональность.

Простите, я не понял вопрос. Зачем мне рантайм-интерфейсы, если я не использую DI? Задачи cross-cutting решаются очень по-разному. Приведите конкретный пример.

Смотрите, программы на Java пишутся не так, как программы на PHP. Программы для бэка пишутся не так, как для фронта. С MySQL нужно работать слегка по-другому, чем с PostgreSQL. На реакте пишут не так, как на ангуляре или на вью. А вы призываете меня смотреть на "проблему" не "через мои привычки", а "объективно"? :)))

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

Это вы уловили.

saveName - это первый урок по программированию для детей.

А вот этот уровень пока не перешагнули.

Простите, я не понял вопрос. Зачем мне рантайм-интерфейсы, если я не использую DI?

Вы не понимаете того, что я написал в своей публикации и для чего я это написал. Более того, вы не пытаетесь понять. Вы даже эксперимент с setupFiles в vitest не поставили.

Я вас уверяю - лично вам DI в JS не нужен. Если кто-то будет пытаться вам навязать внедрение зависимостей в JavaScript, то просто дайте ему ссылку на вот эту нашу переписку.

Продолжайте программировать так, как вам удобно. Я тоже так делаю. Мы все так делаем. И это - не проблема :)

А вот этот уровень пока не перешагнули.

Давайте не будем опускаться до оскорблений. Я пишу эмоционально, но атакую идею, а не вас лично. Если тезис про необъективность задел вас, то объясню, что имею в виду.

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

Не придирайтесь к нюансам аналогии. Я надеюсь, что она объясняет, с каком точки зрения я предлагаю обсуждать вопрос. Задача, способы решения, преимущества и недостатки. Вы привели задачу подмены поведения в тестах. Я показал другое её решение и объяснил, почему на мой взгляд, оно во всём лучше DI. Я бы очень хотел услышать контраргументы и/или примеры других задач, где DI превосходит другие практики. Все статьи про DI, которые мне попадались, фокусируются на задаче тестирования и игнорируют другие способы решения. На вопрос, чем подмена через DI лучше подмены через манкипатчинг, внятного ответа никто не даёт.

Что будет, если сделать import ./storage.js до того, как сделать vi.mock('./storage.js'...)? Например, в setupFiles? Статические импорты - они такие.

Вы даже эксперимент с setupFiles в vitest не поставили.

Можно, пожалуйста, без загадок, а с конкретными примерами конкретных проблем? Я расписал вам пример и аргументацию так чётко, как только мог.

А вот этот уровень пока не перешагнули.

В моей реплике нет ничего оскорбительного - уровни бывают разные. Доктор наук - это тоже уровень. Оскорбительное есть в вашей:

saveName - это первый урок по программированию для детей.

Мне этот пассаж показался несколько неуместным для дискуссии про DI в JS на Хабре и я его отзеркалил. Ваша реакция показывает, что, да - ваша реплика всё-таки содержит некую толику оскорбления и очень хорошо, что вы тоже это заметили.

К сожалению, мне нечего более добавить к моему примеру. Могу только переписать его в более привычном для некоторых виде:

export class SaveNameService {
  constructor({storage}) {
    this.storage = storage;
  }

  async save(name) {
    const value = name.trim().toLowerCase();
    await this.storage.save(value);
    return value;
  }
}

Если вы не понимаете заложенные в этом коде идеи, то вам это и не надо. Для ваших задач это не надо, вот вас и не триггерит.

Поэтому и говорят, что в JavaScript DI не нужен. Хотя лучше сказать, что он нигде не нужен.

И вы ведь не пытаетесь понять, где вы можете применить DI, вы пытаетесь объяснить, почему мне не нужно применять DI. Вам DI нужен примерно нигде, а вы просите, чтобы вас переубедили.

Я лично, на практике, убедился в полезности DI для меня (и в Java, и в PHP), поэтому я пытаюсь добиться аналогичного удобства от JavaScript. Для себя, не для вас. Мне всё равно, каким образом вы решаете стоящие перед вами задачи - каким вам удобно, тем и решаете.

На вопрос, чем подмена через DI лучше подмены через манкипатчинг, внятного ответа никто не даёт.

Даёт, только вы не слышите.

Можно, пожалуйста, без загадок, а с конкретными примерами конкретных проблем? Я расписал вам пример и аргументацию так чётко, как только мог.

Нет, нельзя. У нас тут не учебное заведение. Мы тут меняемся знаниями на взаимовыгодных условиях.

Что будет, если сделать import ./storage.js до того, как сделать vi.mock('./storage.js'...)? Например, в setupFiles? Статические импорты - они такие.

Вот это самый что ни на есть конкретный пример. Вам даже код не надо писать, если вы понимаете, что стоит за этими словами. Но вы не понимаете

Вы даже эксперимент с setupFiles в vitest не поставили.

и не желаете понимать.

Поэтому могу лишь процитировать сам себя:

лично вам DI в JS не нужен

Всего хорошего вам и приятного кодирования!!

Конечно, давайте закончим дискуссию. Разрешите только поясниться за приведённую цитату, поскольку вы в ней увидели что-то оскорбительное.

saveName - это первый урок по программированию для детей.

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

В школе учат школьников, а детей учат в детском саду - держать ложку и ходить на горшок. Я прочитал ваш пассаж, как утверждение, что “DI-сектанты элементарного не понимают - ни как ложку в руках держать, ни как в горшок ходить”.

Вы, разумеется “ничего такого” не имели в виду. Ну так и я “ничего такого” не имел в виду, указав, что вы не смогли самостоятельно увидеть за фабричной функцией:

function createService(...) {
  return ...
}

класс

class Service {
  constructor(...) {...}
}

Просто констатировал этот факт.

Некоторые утверждают, что “класс - это синтаксический сахар над системой прототипов”, а вы назвали это “драматическим усложнением” базовых правил языка. Хотя, на мой взгляд, class - это то, чему “учат в школе”, “классе в 4-м” (если принять первых 5 глав учебника за основы - “детский сад”, как вы, на мой взгляд, предложили основы называть).

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

Вам - не нужен. Пока что, будем надеяться.

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

Публикации