Её идея заключается в том, что высокоуровневая логика не должна зависеть от низкоуровневых реализаций. Бизнес-логике в приложении не должно быть дела до того, получаем ли мы данные из корзины AWS bucket или облачного хранилища Google: у нас должна быть возможность легко менять эти реализации, не ломая программу. Это делает код устойчивым к изменениям. А мы, в свою очередь, можем сделать приложение пригодным для тестирования, меняя эти зависимости на реализации, которые проще тестировать.
Как это делается в Go?
Интерфейсы в Go поддерживают инверсию зависимостей. Мы можем применять различные реализации в коде, если они удовлетворяют определённому интерфейсу, и с помощью внедрения зависимостей указать приложению, какую из них нужно использовать.
Продемонстрируем, как это работает в Go, на примере Quote API, который предоставляет пользователям случайно выбранные цитаты. Вот снимок экрана текущего хендлера Go с цитатами Канье Уэста:
Хендлер прекрасно выполняет свою работу — выдаёт пользователю цитаты Канье Уэста. Он выполняет HTTP-запрос к https://api.kanye.rest, который предоставляет цитату и проверяет валидность ответа. При вызове хендлера мы получаем ответ с цитатой:
Теперь неплохо было бы начать тестирование хендлера, но в настоящее время сделать это без выполнения HTTP-запросов невозможно. Вдобавок мы не можем проверить, что происходит, когда зависимость возвращает плохой ответ. Тут-то и приходит на помощь инверсия зависимостей.
Сначала нужно создать интерфейс, который будет определять поведение зависимости. С его помощью мы сможем подгружать различные реализации с одинаковым поведением. Ниже показан интерфейс:
Любая структура с методом Get
, который принимает строку в качестве аргумента и возвращает указатель на тип ответа из пакета net/http
с ошибкой, будет удовлетворять интерфейсу Client. Именно такое поведение нас и интересует.
Теперь нам нужен способ внедрения зависимости в хендлер, чтобы сделать его инвариантным по отношению к любой реализации интерфейса Client. Рассмотрим некоторые из доступных в Go способов:
Можно создать функцию высшего порядка, которая возвращает исходную функцию хендлера. Это удобно, ведь так мы вызываем только функцию высшего порядка с необходимым элементом. К тому же создавать структуру handler
не придётся.
Затем HTTP-зависимость внедряется из основной функции посредством вызова функции высшего порядка:
Мы можем создать структуру handlers
с функцией конструктора, куда будет добавлена реализация клиента. В этой структуре есть поле под названием client
, в котором будет храниться реализация.
Сделав функцию Kanye
получающим методом handlers
, мы можем получить доступ к полю client
.
Затем внедряем HTTP-клиент из функции main
, создав структуру handlers
с правильными зависимостями.
Недостатком этого подхода является то, что его частое использование приводит к созданию слишком большого числа структур в основном файле.
Другой подход заключается в использовании опций в конструкторе handlers
. Согласно этому подходу, мы устанавливаем реализацию по умолчанию, если пользователь не представляет никакой альтернативы в качестве опции. Конструктор handlers
будет функцией с переменным числом аргументов, так что опции в ней могут быть как заданы, так и не заданы. В приведённом ниже примере мы экспортируем функцию высшего порядка с опциями WithCustomClient
, с которой пользователю будет проще применять альтернативную реализацию Client.
Используя конструктор, мы теперь можем применять реализацию по умолчанию (http.DefaultClient) посредством вызова NewHandlers
без каких-либо аргументов:
Или вызвать его с помощью пользовательской опции Client, тем самым применив реализацию не по умолчанию:
С таким подходом не требуется создавать эти реализации самостоятельно, когда нужно использовать вариант по умолчанию, что сокращает объём основного файла.
Теперь, когда мы рассмотрели различные подходы ко внедрению зависимостей, создадим реализацию интерфейса Client, которая будет использоваться в тестировании. Для внедрения зависимости будем применять функции высшего порядка.
Здесь мы можем либо написать имитированную реализацию самостоятельно, либо использовать GoMock для создания реализации, позволяющей контролировать её поведение с помощью «заглушек». Для этого примера воспользуемся вторым способом.
Добавив строку кода в базу, мы можем автоматически сгенерировать имитированную реализацию из интерфейса Client:
Запуская go generate ./.., мы генерируем следующий код:
Это реализация Client, которую мы можем использовать в тестах, внедрив её вместо стандартного HTTP-клиента и сымитировав выполнение вызовов HTTP Get к API Канье Уэста. Теперь мы можем писать тесты без выполнения HTTP-запросов и проверять, что происходит при получении плохого ответа.
Ниже приведён пример того типа тестового сценария, в котором используется Ginkgo — среда тестирования с разработкой, основанной на поведении.
Тестовый сценарий использует «заглушки» в имитированной реализации client
. Мы проверяем, вызывается ли она правильной строкой URL, и указываем, что необходимо вернуть HTTP-ответ без ошибки. И можем проверить, что код состояния HTTP и цитата, отправленная пользователю, соответствуют нашим ожиданиям.
Мы рассмотрели, как можно использовать инверсию зависимостей для настройки хендлера и задействования его с любой реализацией клиента. Этот метод может применяться к любому типу зависимости.
Инверсия зависимостей может быть мощным инструментом для создания более стабильных и надёжных программ, причём функционал этого инструмента продолжает пополняться. Мы можем создавать тесты, имитирующие зависимости, и уменьшать количество изменений в бизнес-логике, опираясь на один из изложенных подходов.
Все примеры кода можно найти здесь.
Перевод статьи Matthew Cobbing: Using Dependency Inversion in Go
Комментарии