Найти в Дзене
Simple Prog

Внедрение зависимостей в Go

Оглавление
dev.to
dev.to

Давайте поговорим о шаблоне внедрения зависимостей и об управлении зависимостями в больших программах.

Пример с логгером

В любой программе есть main.go, который инициализирует и запускает некоторые службы.

Можно сказать, что не каждая служба в Go реализует всю свою логику. Иногда для этого требуются какие-либо другие сервисы, и они полагаются на них в определенных частях логики.

Например, ведение журнала часто делегируется какому-либо объекту-регистратору, например zap:

Простейший пример реализации сервера на Go с логгером
Простейший пример реализации сервера на Go с логгером

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

В настоящее время наш сервер не ведет журнал сам по себе, сервер полагается на logger. Другими словами, logger стал зависимым от сервера.

Мы сохранили logger как свойство Сервера. Сделав это, мы внедрили logger в качестве зависимости.

Определение

Внедрение зависимостей — шаблон создания сущностей, в результате которого первая (родительская) сущность сохраняется в состоянии второй (зависимой) сущности. Родительская сущность может вызывать зависимую сущность, когда это необходимо.

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

Без изменения состояния базовая программа "hello world" может быть ошибочно распознана как внедрение зависимостей

-3

В основной функции нет состояния, поэтому это не внедрение зависимостей.

Проблемы

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

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

Если существует много связанных объектов, то и кода для их инициализации много. Такой код с правильной логической структурой затрудняет поддержку сервиса.

Сервис с большим количеством зависимостей

Давайте представим, что мы разрабатываем serice, который должен выполнять следующие действия:

  • взаимодействие с базой данных;
  • выполнение вызовов внешних сервисов;
  • регистрация;
  • загрузка и использование config;

Конструктор сервиса должен выглядеть следующим образом:

-4

Кроме того, каждая зависимость от Service требует своей собственной инициализации, для которой могут потребоваться другие объекты. Для

-5

Для создания bankClient нам понадобится cfg и logger.

Теперь давайте представим, что в той же программе необходимо реализовать второй сервис, для которого также требуются db, cfg и logger в качестве зависимостей. Давайте представим схему зависимостей:

-6

Нужно много кода для инициализации первой службы, но нам также нужно инициализировать вторую.

Давайте

Скопируем код инициализации

Мы могли бы просто скопировать и вставить код инициализации db, cfg, logger в service2.

Это сработает, но копировать код - плохая идея. Чем больше поддерживаемого кода, тем больше вероятность ошибки.

Давайте проверим другие варианты.

Реализуем код инициализации для каждого dep

Например, мы можем реализовать функцию инициализации базы данных:

-7

Это выглядит хорошо, и не будет дублирующегося кода инициализации базы данных. Но нам все равно нужно реализовать этот код для каждого повторно используемого dep.

Мы все еще не закончили с getDb - он будет создавать новое соединение для каждого вызова.

Singleton

В случае с db нам нужен единственный экземпляр.

Давайте реализуем это с помощью шаблона Singleton:

-8

Множество Singleton(multiton)

У нас могут быть подключения к разным серверам баз данных, это должны быть отдельные подключения. Но нам все равно нужно, чтобы каждое из них было одноэлементным. Давайте реализуем пул синглтонов — mulition.

Пример реализации Multiton
Пример реализации Multiton

На небольшом количестве сущностей эти шаблоны работают хорошо.

Но если есть десятки типов сущностей, даже такой простой код, как singleton и multiton, сложно реализовать. В этом случае мы могли бы использовать некоторую централизованную логику, которая помогает создавать сущности — инжектор зависимостей.

Контейнер для внедрения зависимостей (injector)

Использование отдельной сущности для создания и хранения других сущностей (injector) довольно распространено во многих языках программирования.

Container реализует логику создания, хранения и получения каждой сущности.

Фокус в программе, использующей container, перемещен с entity и связан с контейнером, что помогает упростить код.

-10

Иногда работа контейнера настолько предсказуема, что можно указать зависимости в декларативном формате — XML, YAML.

В Symfony (PHP) сервис-контейнер является одной из центральных частей фреймворка - даже основные компоненты Symfony предназначены для работы с контейнером.

Symfony поддерживает XML и YAML для объявления.

В Контейнер зависимостей Spring (JAVA) можно настроить с помощью XML или аннотаций.

В GO есть несколько библиотек, реализующих injector по-разному.

Я использовал некоторые из них и подготовил обзор о каждой из них ниже. Исходный код о взаимодействии библиотек di находится в отдельном репозитории на github.

uber-go/dig

dig позволяет нам настраивать контейнер, передавая анонимные функции, и использует пакет reflect.

Для добавления функции инициализации объекта в контейнер следует использовать метод Provide.

Функция должна возвращать желаемый объект или как объект, так и ошибку.

Давайте посмотрим, как мы можем создать регистратор, который зависит от конфигурации. (Это почти оригинальный пример из dig readme).

-11

Используя пакет reflect, dig анализирует типы возвращаемых значений и типы параметров.

С помощью этих данных устанавливаются связи между объектами.

Для получения объекта из контейнера используется метод Invoke:

-12

При создании идентичной сущности необходимо передать параметр name при вызове Provide. В противном случае Provide вернет ошибку.

К сожалению, получить именованную сущность не так просто — в Invoke методе нет параметра name.

В настоящее время для вызова именованных сущностей следует использовать структуру с помеченными полями.:

-13

dig (и каждая библиотека injector здесь) реализует отложенную загрузку объектов. Требуемые объекты создаются только при вызове Invoke.

Можно было бы сказать, что reflect работает медленно, но для контейнера это не имеет значения, потому что обычно контейнер используется один раз при запуске программы.

В результате: проблема с именованными сущностями должна быть задокументирована в dig main readme. В противном случае она отлично работает как инжектор.

elliotchance/dingo

elliotchance/dingo работает совершенно по-другому.

Для генерации кода перехода контейнера необходимо указать конфигурацию YAML. Давайте продолжим с примером конфигурации регистратора. Наш YAML должен выглядеть следующим образом:

-14

Для меня YAML здесь не очень удобен в использовании. Ниже вы увидите, что некоторые части YAML на самом деле могут быть частями кода GO. Но для меня код GO удобен для хранения в файлах *.go — по крайней мере, IDE будет проверять синтаксис go.

Для каждой сущности в YAML, вероятно, нужно указать следующее:

  • imports — список импортируемых библиотек;
  • error — код GO, который должен вызываться при проверке ошибок;
  • returns — часть кода GO, которая инициализирует и возвращает сущность;

Из-за returns я не мог решить: следует ли мне добавить большую часть кода GO в YAML или мне следует создать функцию-конструктор для каждой сущности. Наконец, я перенес всю логику построения конфигурации в NewConfig функцию:

-15

Когда YAML будет готов, нужно установить dingo binary и вызвать его в каталоге проекта —

go get -u github.com/elliotchance/dingo; dingo.

Генерация кода работает быстро. Мне кажется, что большинство настроек из YAML просто напрямую копируются в сгенерированный файл *.go. Таким образом, сгенерированный файл может быть недействительным.

Сгенерированный код помещен в файл dingo.go. Контейнер - это простая структура с полями для каждой сущности с одноэлементной логикой:

-16

В результате: elliotchance/dingo помогает генерировать простой типизированный контейнер из YAML, но при переносе кода GO в YAML ощущается дискомфорт.

sarulabs/di

sarulabs/di выглядит как dig, но не использует reflect. Все объекты в di должны иметь уникальные имена.

Основное отличие заключается в том, что в dig нам не нужно инициализировать зависимости нашей сущности, даже из контейнера — они просто поступают как параметры функции.

В di мы должны извлекать зависимости из контейнера:

-17

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

-18

Но также в sarulabs/di есть бонус — можно указать не только функцию создания, но и функцию перехвата для уничтожения контейнера. Удаление контейнера di начинается с вызова DeleteWithSubContainers и может выполняться при завершении работы программы.

-19

Как я уже упоминал ранее, di не использует reflect, а также не хранит никакой информации о типах сущностей, поэтому мы должны использовать утверждение типа в функции Close, чтобы вернуть логгер к исходному типу.

Также есть дополнительный функционал sarulabs/dingo от того же разработчика, который также обеспечивает строго типизированный контейнер и генерацию кода.

В результате: di — отличный инжектор, но есть некоторая логика копирования кода - для получения зависимости от контейнера.

dig здесь лучше.

google/wire

С помощью wire мы должны поместить код шаблона функции построения для каждой сущности. Мы должны поместить комментарий

//+build wireinject

в начало таких файлов шаблонов.

Затем мы должны запустить

go get github.com/google/wire/cmd/wire; wire

который генерирует файлы *_gen.go для каждого файла шаблона. Сгенерированный код будет содержать реальные функции конструктора, которые генерируются из шаблонов.

Для нашего примера logger-config шаблон конструктора logger будет выглядеть следующим образом:

-20

Сгенерированный код помещается в файл *_gen.go и выглядит следующим образом:

-21

Как и в elliotchance/dingo, в wire есть генерация кода. Но мне не удалось сгенерировать неверный код GO. В каждой ситуации с неверным шаблоном wire выдает ошибки, и код не генерируется.

В wire есть один минус — нам приходится реализовывать шаблон конструктора, используя вызовы пакетов wire. И эти вызовы не так выразительны, как код GO. Поэтому я также переношу всю логику конструктора в функции конструктора, чтобы просто вызывать эти функции конструктора из шаблонов.

Есть полная таблица результатов:

uber-go/dig

  • Формат зависимостей: GO-код, анонимные функции с параметрами
  • Генерация кода GO: Отсутствует
  • Ввод текста: Строгий, но также используется reflect
  • Сокращение кода: максимальное

elliotchance/dingo

  • Формат зависимостей: YAML
  • Генерация кода на GO: Есть
  • Набор текста: Строгий
  • Максимальное сокращение кода, но в YAML есть смесь кода на GO

elliotchance/dingo

  • Формат зависимостей: YAML
  • Генерация кода на GO: Есть
  • Набор текста: Строгий
  • Максимальное сокращение кода, но в YAML есть смесь кода на GO

sarulabs/di

  • Формат зависимостей: GO-код, объявление функций сборки с получением параметров вручную.
  • Генерация кода GO: Нет, но sarulabs/dingo позволяет это.
  • Ввод: Все dep сохраняются в виде interface{}. Но sarulabs/dingo предлагает строго типизированный контейнер.
  • Сокращение кода: Хорошо, но мы должны получить deps из контейнера.

google/wire

  • Формат зависимостей: GO code — шаблоны функций-конструкторов.
  • Генерация кода GO: Да.
  • Набор текста: Строгий.
  • Сокращение кода: максимальное.

Если вам понравилась статья и появились мысли - смело пишите их в комментарии!