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

15 minute read

Перевод статьи “Simple dependency injection in Go with Fx

В Uber очень легко создавать новые приложения. Немалую роль в этом играет Fx - удобная библиотека для внедрения зависимостей. В статье я кратко опишу проблему внедрения зависимостей, как Fx справляется с этой проблемой и покажу пример приложения, которое использует преймущества Fx.

Почему вам может понадобится внедрение зависимостей в Go?

Что такое внедрение зависимостей(DI)? Мне нравится определение со Stack Overflow:

“Внедрение зависомостей” - это 25-ти долларовый термин для 5-ти центовой концепции. Внедрение зависимостей означает предоставление объекту необходимых параметров.

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

Представим, что у нас есть функция, которая выполняет SQL запрос и возвращает результат:

 1func query() (email string) {
 2    db, err := sql.Open("postgres", "user=postgres dbname=test ...")
 3    if err != nil {
 4        panic(err)
 5    }
 6    err = db.QueryRow(`SELECT email FROM "user" WHERE id = $1`, 1).Scan(&email)
 7    if err != nil {
 8        panic(err)
 9    }
10    return email
11}

В этой функции не используется DI. Функция сама создает свои зависимости - подключение к базе данных. Вместо этого, она могла бы принимать подключение как параметр. Такой код невозможно тестировать. DI поможет решить нам проблему тестируемости:

 1func query(db *sql.DB) (email string) {
 2    err = db.QueryRow(`SELECT email FROM "user" WHERE id = $1`, 1).Scan(&email)
 3    if err != nil {
 4        panic(err)
 5    }
 6    return email
 7}
 8
 9func TestQuery(t *testing.T) {
10    db := mockDB()
11    defer db.Close()
12
13    email := query(db)
14    assert.Equal(t, email, "email@example.com")
15}

Такое улучшение тестируемости актуально не только для работы с базой данных, но и применимо к любым кастомным структурам. Вам достаточно определить интерфейсы, с которыми будут работать ваши функции и мокать входящие параметры во время тестирования.

Введение в Fx: фреймворк для внедрения зависимостей для Go

Fx это библиотека от Uber для простого DI в Go. Из описания самого проекта в GoDoc:

Пакет fx - это плтформа, которая позволяет легко создавать приложения из составных и переиспользуемых модулей.

Многие гоферы содрогнутся, прочтя эти строчки. Конечно, мы не хотим тащить Spring и все связанные с ним трудности в Go - язык который пропагандирует простоту создания и поддержки приложений. Моя цель - показать вам, что Fx легковесный и прост в освоении. В этом разделе разберемся с некоторыми типами и функциями, которые предоставляет Fx.

Все Fx приложения начинаются с fx.App которое создается с помощью fx.New(). Минимальное приложение, которое ничего не делает:

1func main() {
2    fx.New().Run()
3}

В Fx реализована концепция “жизненного цикла” приложения. С помощью fx.Lifecycle можно регистрировать функции, которые будут выполнятся при старте и остановке приложения. Хороший пример использования - регистрация http обработчиков.

 1func main() {
 2    fx.New(
 3        fx.Invoke(register),
 4    ).Run()
 5}
 6
 7func register(lifecycle fx.Lifecycle) {
 8    mux := http.NewServeMux()
 9    server := http.Server{
10        Addr: ":8080",
11        Handler: mux,
12    }
13    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
14        w.WriteHeader(http.StatusOK)
15    })
16
17    lifecycle.Append(
18        fx.Hook{
19            OnStart: func(context.Context) error {
20                go server.ListenAndServe()
21                return nil
22            },
23            OnStop: func(ctx context.Context) error {
24                return server.Shutdown(ctx)
25            }
26        }
27    )
28}

В примере выше вы попробовали одну из основных возможностей Fx. Функция register вызывается через метод fx.Invoke(). Как только приложение запускается, lifecycle автоматически предоставляет необходимые параметры для функции register.

Для Fx можно предоставлять свои кастомные конструкторы объектов.

 1func newObject() *object {
 2    return &object{}
 3}
 4
 5func main() {
 6    fx.New(
 7        fx.Provide(newObject),
 8        fx.Invoke(doStuff),
 9    ).Run()
10}
11
12func doStuff(obj *object) {
13    // Do stuff with obj
14}

В Fx есть еще много возможностей для продвинутого DI. Все они описаны в GoDoc.

Пример модульного приложения с Fx

Я написал простое приложение с Fx, которое запускает http сервер. В нем используются общие шаблоны, подходящие для большинства подобных приложений. Например, в приложении создается небольшой, переиспользуемый модуль loggerfx, который предоставляет *zap.Logger.

1var Module = fx.Provide(New)
2
3// --snip--
4
5func New() (*zap.Logger, error) {
6    // --snip--
7}

С помощью Fx можно красиво структурировать код. Хендлеры можно расположить во внутрениих каталогах internal/handler/, например как в hello хенлер. Все хендлеры разом можно пробросить в настройку Fx через общий модуль, описанный в файле internal/handler/module.go:

 1package handler
 2
 3// --snip--
 4
 5var Module = fx.Options(
 6    hello.Module,
 7    user.Module,
 8    // ...
 9)
10
11// In main.go
12fx.New(
13    handler.Module, // this provides all the handlers registered previously
14)

Для запуска приложения достаточно запустить go run main.go.

Заключение

Fx - очень легковесный DI фреймворк, который способствует правильному структурированию кода. Я пользовался им для создания MVCS приложений. На первый взгляд кажется, что приходится писать больше шаблонного кода, но на практике в коде становится проще ориентироваться. И самое главное - приложение становится проще тестировать.