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

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

Проблема с DI-контейнерами в Go в том, что неочевидно, а реальна ли вообще проблема, которую они пытаются решить… и если даже реальна, то неочевидно решают ли они её, или просто создают новую проблему вместо старой (вот эта вот вся магия, неочевидный код, сложности с тестированием, освобождением ресурсов, синглтонами, разными реализациями одного интерфейса, etc.).

Можно заменить «контейнер» на «фреймворк» и всё будет в силе, но подойдёт под большее число случаев.

А в го необязательно их использовать. Отлично работает и "ручное" построение графа зависимостей.
Я описывал примеры такого подхода в своей статье на ту же тему вот здесь: https://m.habr.com/ru/company/vivid_money/blog/531822/

Ручное построение графа зависимостей можно разбить на составляющие подчасти, чтобы не было большой лапши где-то в main. У нас в проекте я решил делать так: для каждого корневого пакета завёл build.go с единственной функцией Build. Эта функция по сути "строит пакет". На входе принимает структуру Config с набором интерфейсов, на выходе — структуру BuildResult с набором полученных сервисов. Внутри функции — простое ручное построение (т.к. построение только в рамках пакета, то там кода не много). Функция Build может дальше делать запросы в подпакеты, в такие же соответствующие функции Build. В итоге это более-менее разумно компонуется. Но сервис у нас не большой, так что не могу точно утверждать, насколько это масштабируется. Самый корневый Build вызывается в main. В BuildResult может возвращаться интерфейс Stopper, на который делается defer для graceful shutdown. Эти интерфейсы можно тоже композировать.

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

По субъективному опыту, проблему с DI-контейнерами в Go испытывают те, кто активно пользовался этим в языках вроде Java, где это действительно нужно.


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


Другое дело, если речь идет о DI в монолите, но опять же, по опыту, компании, применяющие Go для написания монолитов ни о каком DI не задумываются...

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

Скорее всего как-то так: большая контора типа убера или гугла начинает использовать го и бросает на него какие-то существующие отделы (не набирать же с улицы, если ты убер или гугл). Эти отделы писали до этого на чем-то вроде джавы или пхп, и у них сгорает стул оттого, что в Go нельзя делать также. Они пилят свои велосипеды, это становится корпоративным стандартом, а потом попадает в опенсорс. Ну а убер или гугл в опенсорс финги не выложат — думают другие новички в Go, и пошло-поехало.

НЛО прилетело и опубликовало эту надпись здесь
Это может оказаться проблемой для разделяемых транзитивных зависимостей: если, скажем, клиент базы данных и консьюмер сообщений оба зависят от логгера, который кроме них больше никому не нужен, то оба объекта не могут включить его провайдер в свой Provider Set — в этом случае возникнет конфликт между двумя провайдерами для одного типа.

А почему не создавать логгер отдельным провайдером?


func ProvideLogger() (logger Logger, cleanup func(), err error)
func ProvideDBClient(logger Logger) (dbClient DbClient, cleanup func(), err error)
func ProvideConsumer(logger Logger) (consumer Consumer, cleanup func(), err error)
func ProvideAll() (dbClient DbClient, consumer Consumer, cleanup func(), err error)

Если ProvideAll() не устраивает, и надо клиента и консьюмера создавать отдельно — что ж, тогда придется logger инициализировать заранее и передавать в провайдеры вручную.


Проблема паники, впрочем, действительно, легко не решается. Кроме того, чтобы эту панику "ловить" во всех providers, ничего и не приходит в голову. Однако, паниковать в Go не принято, см. тот же Uber:


Panic/recover is not an error handling strategy. A program must panic only when something irrecoverable happens

Uber Go Style Guide
А почему не создавать логгер отдельным провайдером?..


Я подразумевал, что он так и создаётся. Но ProvideAll связывает сущности, которые в общем-то ничем не связаны, кроме похожих зависимостей. Кроме того, вот в такой ситуации:

— Logger
— MetricCollector
— System (depends on: MetricsCollector)
— DBClient (depends on: Logger)
— MessageConsumer (depends on: Logger, MetricCollector)
— HTTPServer (depends on: MetricCollector)
— Application (depends on: System, DBClient, MessageConsumer, HTTPServer)

нельзя сделать какой-то один ProvideAll — для получения Application провайдеры для Logger и MetricCollector нужно указать явно, даже если эти зависимости для него транзитивны. Грубо говоря, создавая объект, нужно знать в точке его создания обо всех зависимостях его зависимостей, зависимостях их зависимостей и так далее. ProviderSet решает эту проблему, но лишь отчасти.

Однако, паниковать в Go не принято.


Я ни в коем случае не призываю использовать панику в качестве механизма обработки ошибок! :) Однако паники иногда всё же случаются, даже если нигде явно не писать panic. И, например, вот эта локальная база сломается, если её корректно не закрыть.
Грубо говоря, создавая объект, нужно знать в точке его создания обо всех зависимостях его зависимостей, зависимостях их зависимостей и так далее.

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

Про Service Locator написана ерунда полная. Это в прицнипе намного более простой паттерн, а не какая не альтернатива той магии, которую дальше описывает автор. В вики по ссылке написаны такие минусы, которые можно отнести ко многим системам, которые делают непрямое создание экземплаторв класса централизованное.

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

Публикации

Истории