Как не получится-то? Чтобы “создать в одном контексте, а обратиться в другом”, нужно созданный объект между асинхронными контекстами каким-то образом передать (через какой-то внешний shared state). Если пользоваться по конвенции (вызывать функцию-ambient locator в месте использования, и передавать объект исключительно по стеку вниз), то получить проблему не получится.
Ровно так же можно передать или сохранить объект, полученный из одного this.$ в другой контекст (вместо того, чтобы там еще раз обратиться к this.$) и работать с неправильным логгером. Это ровно одинаковые проблемы. Просто ты привязался к this, а я к асинхронному контексту.
А, нет, не признали.
Не признал что именно? Напомню, в статье указаны два несуществующих минуса:
❌ Изменение глобального состояния меняет реализацию сразу для всего приложения, что может дать неожиданные сайд эффекты. — Это неверно, т.к. в случае ambient service locator глобального состояния не существует; состояние привязано к контексту выполнения. Если контекст подменяет useS3, то любой код, работающий на время действия подмены (внутри колбека withOverrides()), через useS3() всегда получит тестовый клиент, но параллельно работающий код продолжает через ровно ту же самую useS3() получать настоящий.
❌ Много бойлерплейта с оверрайдами в прикладной логике. — Нет, бойлерплейта, как я указал (а ты не опроверг), нет вообще. То есть буквально 0. Вызываешь функцию и получаешь зависимость. Оверрайд — буквально вызов функции withOverrides с указанием чего, собственно, оверрайдить.
Отсутствие этих минусов в реализации ambient service locator показывает, что “непротиворечивая классификация DI” противоречива.
Я же не говорю, что у меня вышло идеальное решение, а cDI — лажа. В конце концов, я верю, что кому-то нравится возиться с явным this и огромной динамически меняющейся свалкой внутри него, пусть возится. Мне лично ближе чистые функции с явным контрактом (а DI уже как механизм их скрытой подмены). Я говорю о том, что “классификация” забыла про еще один архитектурный вариант и побила “соломенные чучела”.
Предлагаю продолжить, когда вы свой нейрослоп опробуете в бою.
Так он в бою, обслуживает два коммерческих SaaS. Основная выгода получилась при организации тестирования (контекстная подмена сервисов одной строчкой без изменения продакшен-кода), но не только. Например, DI-шов useTenant() позволяет в SaaS в параллельных потоках обслуживать разных клиентов (данные которых лежат в разных БД на разных доменах), а при сборке onprem-версии той же неизменной кодовой базе (меняется только самая верхняя точка композиции, ровно 1 файл) работать в single-tenant режиме.
Создал объект в одном контексте, а обратился к его методу из другого
Такой футган можно в любой DI-архитектуре допустить. Не надо зависимости передавать и сохранять случайным образом, всё равно есть какая-то конвенция на lifecycle, вопрос в том сильно ли она церемониальная или нет.
При “нормальной” работе (нужна зависимость - позвал геттер, сложил в локальную переменную) такой проблемы нет и не будет.
А с мемоизацией, зависящих от стека вызова
Мемоизацию просто тоже нужно иметь scope-aware. Я добавил, ради прикола: и как декоратор для методов, и как standalone memoize хелпер.
В любом случае, мой поинт в том, что так или иначе между gDL и cDL есть ещё вариант ambient/async scoped lookup, который в заявленную “непротиворечивую” классификацию не помещается. В ripple один и тот же объект при вызове из разных async scopes намеренно видит разные зависимости, при этом параллельные потоки друг на друга не влияют, и в принципе все указанные в табличке для gDL минусы (кроме необходимости асинхронного рантайма) отсутствуют.
Не совсем. Сами зависимости (перечень изменяемых элементов) — глобальны, но их значения — полностью локальны внутри асинхронного контекста. Аргумент “Изменение глобального состояния меняет реализацию сразу для всего приложения” невалиден: подмены глобального состояния в принципе нет. Равно как и аргумент “Много бойлерплейта с оверрайдами в прикладной логике” — это соломенное чучело, бойлерплейта нет ВООБЩЕ: вызовы зависимостей имеют ноль бойлерплейта (это просто вызовы функций), а временная подмена — это максимально сжатый и понятный код, который читается буквально как “подменить X на X’ и вызвать функцию”.
Вот конкретно твой пример переписан:
// Реализации по умолчанию (опционально):
const Log = defineFactoryDependency(
(scope: string) => new ConsoleLog(scope),
)
const Net = defineFactoryDependency(
(scope: string) => new HttpNet(scope),
)
const Auth = defineFactoryDependency(
(scope: string) => new LocalAuth(scope),
)
class Form extends Object {
scope = 'Form'
@mem get log() { return Log(this.scope) } // context lookup
@mem get net() { return Net(this.scope) } // context lookup
send(payload: string) {
this.log.info('Sending', payload)
this.net.request(payload)
}
}
class SignIn extends Form {
scope = 'SignIn:' + this.auth.id + ':' + super.scope
@mem get auth() { return Auth(this.scope) } // context lookup
submit() {
this.send(this.auth.key)
this.log.info('Signed in', this.auth.id)
}
}
// Оверрайды на время запуска функции:
await withOverrides([
provide(Log, scope => new MemLog(scope)),
provide(Net, scope => new LocalNet(scope)),
provide(Auth, scope => new FakeAuth(scope)),
], () => new SignIn().submit()) // isolated run
В прикладном коде при этом нет ни this.$, ни прокидывания контекста, ни временных глобальных оверрайдов, которые “могут дать неожиданные сайд эффекты”.
cDL — Обращение через локальный контекст, инжектируемый из вне.
Это подгон определения под твоё решение. Локальный контекст может не инжектироваться, а лежать в AsyncLocalStorage.
Я на днях навайбкодил библиотеку (ripple-di) как раз в таком стиле: вместо this.$ берется асинхронный контекст и строится автоматический граф зависимостей при локальных оверрайдах.
Buefy не работает в Vue 3 (есть только полуготовый неофициальный форк). Автор Buefy написал для Vue 3 новый проект Oruga, избавившись при этом от жесткой привязки к Bulma, но оставив Bulma как одну из поддерживаемых тем.
Там-там это, вроде, фактически клиент одноклассников. База сообщений там общая. Ну и сделан он весьма хорошо, не удивлюсь, если там аудитория есть (из тех же одноклассников). К примеру, он очень быстрый, даже наверное быстрее телеги, не похож на типовой продукт от Мейл ру. Скорее похож не на китайский клон, а на корейский. Есть АПИ для ботов и вообще достаточно проработанный клон.
Предложенный вами упрощенный подход работает только в простом случае единого системного интерпретатора node и не будет работать при использовании современных систем управления версиями интерпретаторов, таких как mise.
Например, типовой скрипт pnpm lint-staged упадет с ошибкой:
.git-hooks/pre-commit: line 2: pnpm: command not found
в то время как с husky он не упадет, так как husky имеет свою точку входа, которая запускает дополнительную инициализацию через ~/.huskyrc.
Ни-че-го.
Вы забыли прикрепить еще содержимое собственно точки входа husky (.husky/_/h), которая как раз и добавляет эту ценность.
P.S. Не следует использовать postinstall, нужно использовать prepare. postinstall выполняется в том числе когда ваш модуль устанавливается как зависимость для другого модуля, то есть при этом он будет или показывать ложное сообщение об ошибке, или портить конфиг гита пользователям.
Предложенный мною вариант лучше тем, что первая из двух строк (создание обработчика вызовом serverless(app)) вызовется ровно один раз за время жизни программы. Я же написал: "закешировать [в константе] на уровне модуля".
В предложенном же автором коде новый обработчик создается при обработке каждого запроса, обрабатывает ровно один запрос и затем уничтожается сборщиком мусора. Это медленнее.
1) Вместо path: event.url нужно писать path: event.url.replace(/\?.*/, ""), иначе это нарушает спецификацию (и OpenAPI, и Express). @koa/router вообще не работает без этого, например.
2) Вместо return serverless(app)(patchedEvent, context) нужно, конечно, закешировать на уровне модуля const _handler = serverless(app) и затем вызывать return _handler(patchedEvent, context)
3) Вместо get: в OpenAPI спецификации нужно использовать x-yc-apigateway-any-method:, чтобы работали POST, PUT и т.д.
Как не получится-то? Чтобы “создать в одном контексте, а обратиться в другом”, нужно созданный объект между асинхронными контекстами каким-то образом передать (через какой-то внешний shared state). Если пользоваться по конвенции (вызывать функцию-ambient locator в месте использования, и передавать объект исключительно по стеку вниз), то получить проблему не получится.
Ровно так же можно передать или сохранить объект, полученный из одного
this.$в другой контекст (вместо того, чтобы там еще раз обратиться кthis.$) и работать с неправильным логгером. Это ровно одинаковые проблемы. Просто ты привязался кthis, а я к асинхронному контексту.Не признал что именно? Напомню, в статье указаны два несуществующих минуса:
❌ Изменение глобального состояния меняет реализацию сразу для всего приложения, что может дать неожиданные сайд эффекты. — Это неверно, т.к. в случае ambient service locator глобального состояния не существует; состояние привязано к контексту выполнения. Если контекст подменяет
useS3, то любой код, работающий на время действия подмены (внутри колбекаwithOverrides()), черезuseS3()всегда получит тестовый клиент, но параллельно работающий код продолжает через ровно ту же самуюuseS3()получать настоящий.❌ Много бойлерплейта с оверрайдами в прикладной логике. — Нет, бойлерплейта, как я указал (а ты не опроверг), нет вообще. То есть буквально 0. Вызываешь функцию и получаешь зависимость. Оверрайд — буквально вызов функции
withOverridesс указанием чего, собственно, оверрайдить.Отсутствие этих минусов в реализации ambient service locator показывает, что “непротиворечивая классификация DI” противоречива.
Я же не говорю, что у меня вышло идеальное решение, а cDI — лажа. В конце концов, я верю, что кому-то нравится возиться с явным
thisи огромной динамически меняющейся свалкой внутри него, пусть возится. Мне лично ближе чистые функции с явным контрактом (а DI уже как механизм их скрытой подмены). Я говорю о том, что “классификация” забыла про еще один архитектурный вариант и побила “соломенные чучела”.Так он в бою, обслуживает два коммерческих SaaS. Основная выгода получилась при организации тестирования (контекстная подмена сервисов одной строчкой без изменения продакшен-кода), но не только. Например, DI-шов
useTenant()позволяет в SaaS в параллельных потоках обслуживать разных клиентов (данные которых лежат в разных БД на разных доменах), а при сборке onprem-версии той же неизменной кодовой базе (меняется только самая верхняя точка композиции, ровно 1 файл) работать в single-tenant режиме.Такой футган можно в любой DI-архитектуре допустить. Не надо зависимости передавать и сохранять случайным образом, всё равно есть какая-то конвенция на lifecycle, вопрос в том сильно ли она церемониальная или нет.
При “нормальной” работе (нужна зависимость - позвал геттер, сложил в локальную переменную) такой проблемы нет и не будет.
Мемоизацию просто тоже нужно иметь scope-aware. Я добавил, ради прикола: и как декоратор для методов, и как standalone memoize хелпер.
В любом случае, мой поинт в том, что так или иначе между gDL и cDL есть ещё вариант ambient/async scoped lookup, который в заявленную “непротиворечивую” классификацию не помещается. В ripple один и тот же объект при вызове из разных async scopes намеренно видит разные зависимости, при этом параллельные потоки друг на друга не влияют, и в принципе все указанные в табличке для gDL минусы (кроме необходимости асинхронного рантайма) отсутствуют.
Не совсем. Сами зависимости (перечень изменяемых элементов) — глобальны, но их значения — полностью локальны внутри асинхронного контекста. Аргумент “Изменение глобального состояния меняет реализацию сразу для всего приложения” невалиден: подмены глобального состояния в принципе нет. Равно как и аргумент “Много бойлерплейта с оверрайдами в прикладной логике” — это соломенное чучело, бойлерплейта нет ВООБЩЕ: вызовы зависимостей имеют ноль бойлерплейта (это просто вызовы функций), а временная подмена — это максимально сжатый и понятный код, который читается буквально как “подменить X на X’ и вызвать функцию”.
Вот конкретно твой пример переписан:
В прикладном коде при этом нет ни
this.$, ни прокидывания контекста, ни временных глобальных оверрайдов, которые “могут дать неожиданные сайд эффекты”.Это подгон определения под твоё решение. Локальный контекст может не инжектироваться, а лежать в AsyncLocalStorage.
Я на днях навайбкодил библиотеку (ripple-di) как раз в таком стиле: вместо
this.$берется асинхронный контекст и строится автоматический граф зависимостей при локальных оверрайдах.Buefy не работает в Vue 3 (есть только полуготовый неофициальный форк). Автор Buefy написал для Vue 3 новый проект Oruga, избавившись при этом от жесткой привязки к Bulma, но оставив Bulma как одну из поддерживаемых тем.
Там-там это, вроде, фактически клиент одноклассников. База сообщений там общая. Ну и сделан он весьма хорошо, не удивлюсь, если там аудитория есть (из тех же одноклассников). К примеру, он очень быстрый, даже наверное быстрее телеги, не похож на типовой продукт от Мейл ру. Скорее похож не на китайский клон, а на корейский. Есть АПИ для ботов и вообще достаточно проработанный клон.
Предложенный вами упрощенный подход работает только в простом случае единого системного интерпретатора node и не будет работать при использовании современных систем управления версиями интерпретаторов, таких как mise.
Например, типовой скрипт
pnpm lint-stagedупадет с ошибкой:в то время как с husky он не упадет, так как husky имеет свою точку входа, которая запускает дополнительную инициализацию через
~/.huskyrc.Вы забыли прикрепить еще содержимое собственно точки входа husky (
.husky/_/h), которая как раз и добавляет эту ценность.P.S. Не следует использовать
postinstall, нужно использоватьprepare.postinstallвыполняется в том числе когда ваш модуль устанавливается как зависимость для другого модуля, то есть при этом он будет или показывать ложное сообщение об ошибке, или портить конфиг гита пользователям.dayjs это а) монолит, а не tree shakeable набор утилит, б) тащит за собой доморощенные локали, а не использует Intl.DateTimeFormat.
Над Intl.DateTimeFormat вообще нет хороших врапперов, приходится свои писать. Поэтому начинание хорошее.
Предложенный мною вариант лучше тем, что первая из двух строк (создание обработчика вызовом
serverless(app)) вызовется ровно один раз за время жизни программы. Я же написал: "закешировать [в константе] на уровне модуля".В предложенном же автором коде новый обработчик создается при обработке каждого запроса, обрабатывает ровно один запрос и затем уничтожается сборщиком мусора. Это медленнее.
Несколько замечаний:
1) Вместо
path: event.urlнужно писатьpath: event.url.replace(/\?.*/, ""), иначе это нарушает спецификацию (и OpenAPI, и Express). @koa/router вообще не работает без этого, например.2) Вместо
return serverless(app)(patchedEvent, context)нужно, конечно, закешировать на уровне модуляconst _handler = serverless(app)и затем вызыватьreturn _handler(patchedEvent, context)3) Вместо
get:в OpenAPI спецификации нужно использоватьx-yc-apigateway-any-method:, чтобы работали POST, PUT и т.д.