Comments 15
сервис стартует, он регистрирует все свои обработчики запросов, тем самым создавая сразу всё дерево зависимостей
Для тестирования компонентов как раз зачастую хочется строить поддерево, часто с заменой компонентов на заглушки.
я непонял: для тестов у Вас есть отдельная конфигурация контейнера? в ней только нужная ветка с заглушками? или как?
Если нужен функциональный или интеграционный тест, то скорее всего вы однимите сервис целиком, как "черный ящих" и мокать нужно будет только внешние запросы в БД или другие сервисы. В остальном работают Unit-тесты.
Или я что-то не понимаю? Напишие ситуацию подробнее, интересно понять.
Если у вас каждый пакет будет изолирован, то есть принимать внешние зависимости через локальный интерфейс, то и не придется для юнит-тестов строить граф зависимостей, и тесты сильно упростятся.
Если вам нужно использовать DI-контейнер с частично инициализированными зависимостями - вы точно делаете что-то не то я думаю.И это не связано с языком программирования.
Зависимости «живут», пока «живёт» сервис, ничего удалять и повторно создавать не нужно
Как минимум, на одном соединении к СУБД вы долго не протянете и надо будет их создавать и удалять. Причём бездумно не получится, потому что есть такая штука как транзакции, запросы которых должны быть в рамках одного соединения. И вот у вас уже есть поддерево зависимостей, которое должно создаваться и удаляться динамически. И таких кейсов может быть много. Посмотрите на реализации DI/IoC в Java/C#, там приложения тоже запускаются один раз и всё, но разработчикам дают выбор скоупа зависимостей
...Что будет с начинающими разработчиками, я не представляю. А мы ведь хотим просто внедрить зависимости.
Имею неудачный опыт uber/fx на новом для меня проекте. Скажу, что вместо того, чтобы пронумеровать модули и последовательно изучать, в такой темной магии приходится знать ВСЁ-И-СРАЗУ. Что невозможно и превращается в НИ-ЧЕ-ГО. Я читал отдельные функции, но пока стало выстаиваться в какую-то картину закончился испытательный срок.
Аргументация типа "раньше было еще хуже". Но для меня uber/fx теперь синоним того, что бэкенд решил не напрягаться с архитектурой проекта.
Но никак не могу понять, зачем он в микросервисах и даже сервисах большого размера на Go.
Простой вопрос, простой ответ - потому что модно, потому что все так делают. Предположу что есть взять современного разработчика и попросить его написать проект без DI то многие впадут в ступор. По крайне мере я таких встречал. Потому что никто не хочет задумываться зачем и почему он что-то делает, все так делают и я делаю. Соглашусь с Вами, проблема которую решает DI слишко преувеличина и сам DI часто создает проблем не меньше чем их решает а то и больше. Сходить один раз прописать конфигурацию не такой уж и подвиг, зато не нужно тратить время на изучение модных библиотек или еще хуже сидеть неделю разбирается почему чудо-библиотека на рефлексии не внедряет то что внедрить нужно.
Потому что го позиционируется как язык, который можно очень быстро изучить и сразу писать код. Это приводит к тому, что куча людей пишет код сервиса на го, по факту изучив только основы, и не вникая, ни в то, как писать идиоматично для языка, ни как нормально структурировать код. На выходе получается очень сильно связанное спагетти. Которое невозможно ни править, ни покрывать юнит-тестами. Добавляем сверху идиотское корпоративное требование 90% покрытия юнит-тестами, и получаем огромные уродливые тесты, которые ничего, на самом деле внятно не тестируют (потому что с такой связностью практически невозможно изолировать отдельные модули и тестировать их отдельно), но при этом тесты будут неочевидно ломаться от почти любых изменений в коде.
DI для нормальной работы требует какой-никакаой структуры кода и разделения на слабо-связанные модули. Что позволяет писать на эти модули нормальные тесты, сильно упрощает рефакторинг, а еще позволяет нормально декомпозировать код инициализации/деинициализации сервиса и заменить огромную функцию (1-2k строк), которая создает и связывает вместе все компоненты, попутно обрабатывая миллион ошибок, потому что надо, на внятное декларативное описание, какие модули нужно инициализировать, не вдаваясь в подробности порядка инициализации, конкретных зависимостей каждого модуля, и обработки каждой возможной ошибки. Плюс это декларативное описание получается без ветвлений, так что проверить инициализацию приложения тестами гораздо проще, чем без DI.
Возможность использовать провайдеры конкретного модуля в тестах на этот модуль — отдельное счастье, которое уменьшает количество дополнительного кода, которое нужно написать, чтобы запустить тест.
Успешно внедрил uber fx в нескольких местах, и собираюсь внедрить еще в нескольких, когда дойдут руки.
Которое невозможно ни править, ни покрывать юнит-тестами. Добавляем сверху идиотское корпоративное требование 90% покрытия юнит-тестами, и получаем огромные уродливые тесты, которые ничего, на самом деле внятно не тестируют (потому что с такой связностью практически невозможно изолировать отдельные модули и тестировать их отдельно), но при этом тесты будут неочевидно ломаться от почти любых изменений в коде.
Второй человек пишет про Юнит-тесты. Но я вот не понимаю, причем тут DI-контейнер? Тесты они ведь Юнит, потому что должны тестировать юнит без контейнера. Когда считается покрытие, то счетчику вообще пофигу используете вы DI или нет. Или у вас не так?
Вы используете DI, что бы удобно писать тесты: в тестах вы "поднимаете" весь DI, частично мокаете и потом запускаете тест. И так для каждого теста. Если в контенере 1000 сервисов, нужно замокать 1, вы его мокаете и запускаете тест. Так получается?
Тоже хотел написать про тесты, но видимо проще ответить :)
Потому что без какого-либо контроля зависимостей код может (и будет, скорее всего) получиться сложно тестируемым. Если у вас нет четких модулей, которые можно подменить например или вообще выбросить на время теста, то и тестировать вы будете не модули, а слишком общие сущности.
Используя DI вы можете любой модуль/сервис/зависимость протестировать отдельно, потому что реализация зависимостей не зашита жестко в код.
Я лично пришел к тому что в Го это нахер не нужно, как вы и пишите в статье. Просто при этом нужен здравый смысл: не терять читабельность, понятность и возможно тестируемость кода (хотя я пишу для себя и тестирую очень мало по корпоративным меркам, поэтому мне с этим проще)
Я просто декомпозицию инициализацию/деинициализацию с помощью di, у меня каждому модулю соответствует функция, которая регистрирует этот модуль в DI, включая конструкторы для всех компонентов этого модуля, все внутренние связи, а также весь код для запуска/остановки фоновых горутин. Соответственно в тестах, я беру di, пихаю в него описание одного модуля, а также моки/фикстуры вместо его зависимостей. Запускаю di, прогоняю тесты, останавливаю di. Отдельно могу запустить весь сервис со всеми компонентами, и замокать внешние зависимости, чтобы проверить, что все вместе работает как ожидается.
Главное тут то, что чтобы использовать DI нужно разделить код на отдельные слабо-связаные компоненты, а это очень важно для написания юнит-тестов, которые реально проверяют поведение, не фиксируют имплементацию.
Проблема с тестами решается проще чем изучение uber-fx и затаскивание его в проект.
Об этом здесь "Еще раз про Di-контейнеры в golang" https://habr.com/ru/articles/903300/
Мы переходили на Go видя его в срезе: скорость, типизация. И когда основной приоритет и задача - скорость, само собой пишем все самостоятельно.
К подобным вариантам отношусь с толикой скептицизма. С одной стороны комфорт в моменте здесь и сейчас, с другой провал на длинной дистанции. Особенно это заметно на проектах где большие монолитные приложения, которые набрали столько чужих зависимостей, что потом месяцами ищут утечку памяти или простую потерю производительности. Хотя стоит заметить, в основном это не по причине того что тот или иной чужой «пакет» криво сделан, а потому что сам разработчик в погоне за упрощением (а тут лучше читать как: так упрощал жизнь) забыл язык или изначально не знал специфики.
Ниже приведено чисто мое мнение, выработанное практикой.
В Go принято писать инициализацию:
руками;
использовать кодогенерацию.
Из этого следует, что если у вас проявилась причина использовать всякие DI-контейнеры и прочие «autowire», высока вероятность, что у вас уже не микросервис, а прям сервис, и причем монолитный.
Были статьи тут на ресурсе про контейнеры, и на один из вопросов, что если там, с учетом того, что магия работает на интерфейсах 1 типа, в котором может привязаться совсем не то, что ожидалось, автор ответил, что будем писать тест на контейнер (facepalm).
В приложении Go обычно разделяют процесс конфигурирования, сборки пакетов в приложение и запуск (graceful shutdown). Пакеты, и в том числе app, имеют внешние зависимости как локальные интерфейсы, а конструктор (функция New) должна выдать неэкспортируемую структуру-сервис для исключения прямой зависимости между пакетами.
Как, например, можно в команде принять такой вариант:
cmd/app/main.go — точка старта приложения, обычно тут app.New(cfg), app.Start(ctx), graceful shutdown.
internal/app/app.go — тут сборка пакетов в приложение, и тут New, Start(ctx).
internal/app/services.go — инициализация сервисов.
internal/app/consumers.go — инициализация консьюмеров.
internal/config/ — тут конфиг приложения.
internal/services/servcie1 — пакеты, используемые в приложении, и максимально изолированы.
internal/services/service2
То есть вместо этого
func provider[T any](factoryFunc func() T) func() T {
providerId := uuid.New()
...........
-----------------------
var useFetchItemsService = provider(
func() domain.Command[dto.SearchCriteria, *dto.ItemsCollection] {
return service.NewFetchItemsService(
// ... Ещё зависимости
)
},
)
.......................
будет
app.go
-------
type service1 interface {
func DoIt(ctx) (string, error)
}
type service2 interface {
func DoNotDoIt(ctx) error
}
type app struct {
s1 service1
s2 service2
}
-------
app/services.go
func (a *app) initService1(ctx context.Context) error {
s1, err := service1.New(ctx)
if err != nil {
return err
}
a.s1 = s1
return nil
}
---------------
services/service1.go
type service struct {}
func New(ctx context.Context) (*service, error) {
тут логика инициализации сервиса
}
Шаги конфигурации, инициализации и запуска ясно видно и легко можно поменять логику(если необходимо)
Да, если у вас более 200 сервисов и зависимостей со сложными графами, то такое не подходит. Но это, по-моему, является ярким признаком, что у вас в контекст микросервиса много запихано бизнес-функций. И это также становится причиной, что нужно будет использовать ORM и другие штуки из мира большого «кровавого энтерпрайз» java/php/c# мира.
Расскажите, зачем вам DI-контейнер в golang