Внедрение зависимостей с Fx
Перевод статьи “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 приложений. На первый взгляд кажется, что приходится писать больше шаблонного кода, но на практике в коде становится проще ориентироваться. И самое главное - приложение становится проще тестировать.