Перевод статьи “Simple dependency injection in Go with Fx“
В Uber очень легко создавать новые приложения. Немалую роль в этом играет Fx - удобная библиотека для внедрения зависимостей. В статье я кратко опишу проблему внедрения зависимостей, как Fx справляется с этой проблемой и покажу пример приложения, которое использует преймущества Fx.
Что такое внедрение зависимостей(DI)? Мне нравится определение со Stack Overflow:
“Внедрение зависомостей” - это 25-ти долларовый термин для 5-ти центовой концепции. Внедрение зависимостей означает предоставление объекту необходимых параметров.
Проще говоря, DI - это предоставление необходимых объекту зависимостей. В интернете можно найти кучу информации о DI, в которых эта концепция описывается намного лучше. Мне не хочется углубляться и перегружать вас терминами, поэтому я сконцентрируюсь на самом важном аспекте - упрощения тестирования с помощью DI.
Представим, что у нас есть функция, которая выполняет SQL запрос и возвращает результат:
func query() (email string) {
db, err := sql.Open("postgres", "user=postgres dbname=test ...")
if err != nil {
panic(err)
}
err = db.QueryRow(`SELECT email FROM "user" WHERE id = $1`, 1).Scan(&email)
if err != nil {
panic(err)
}
return email
}
В этой функции не используется DI. Функция сама создает свои зависимости - подключение к базе данных. Вместо этого, она могла бы принимать подключение как параметр. Такой код невозможно тестировать. DI поможет решить нам проблему тестируемости:
func query(db *sql.DB) (email string) {
err = db.QueryRow(`SELECT email FROM "user" WHERE id = $1`, 1).Scan(&email)
if err != nil {
panic(err)
}
return email
}
func TestQuery(t *testing.T) {
db := mockDB()
defer db.Close()
email := query(db)
assert.Equal(t, email, "email@example.com")
}
Такое улучшение тестируемости актуально не только для работы с базой данных, но и применимо к любым кастомным структурам. Вам достаточно определить интерфейсы, с которыми будут работать ваши функции и мокать входящие параметры во время тестирования.
Fx это библиотека от Uber для простого DI в Go. Из описания самого проекта в GoDoc:
Пакет fx - это плтформа, которая позволяет легко создавать приложения из составных и переиспользуемых модулей.
Многие гоферы содрогнутся, прочтя эти строчки. Конечно, мы не хотим тащить Spring и все связанные с ним трудности в Go - язык который пропагандирует простоту создания и поддержки приложений. Моя цель - показать вам, что Fx легковесный и прост в освоении. В этом разделе разберемся с некоторыми типами и функциями, которые предоставляет Fx.
Все Fx приложения начинаются с fx.App
которое создается с помощью fx.New()
. Минимальное приложение, которое ничего не делает:
func main() {
fx.New().Run()
}
В Fx реализована концепция “жизненного цикла” приложения. С помощью fx.Lifecycle
можно регистрировать функции, которые будут выполнятся при старте и остановке приложения. Хороший пример использования - регистрация http обработчиков.
func main() {
fx.New(
fx.Invoke(register),
).Run()
}
func register(lifecycle fx.Lifecycle) {
mux := http.NewServeMux()
server := http.Server{
Addr: ":8080",
Handler: mux,
}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
lifecycle.Append(
fx.Hook{
OnStart: func(context.Context) error {
go server.ListenAndServe()
return nil
},
OnStop: func(ctx context.Context) error {
return server.Shutdown(ctx)
}
}
)
}
В примере выше вы попробовали одну из основных возможностей Fx. Функция register
вызывается через метод fx.Invoke()
. Как только приложение запускается, lifecycle
автоматически предоставляет необходимые параметры для функции register
.
Для Fx можно предоставлять свои кастомные конструкторы объектов.
func newObject() *object {
return &object{}
}
func main() {
fx.New(
fx.Provide(newObject),
fx.Invoke(doStuff),
).Run()
}
func doStuff(obj *object) {
// Do stuff with obj
}
В Fx есть еще много возможностей для продвинутого DI. Все они описаны в GoDoc.
Я написал простое приложение с Fx, которое запускает http сервер. В нем используются общие шаблоны, подходящие для большинства подобных приложений. Например, в приложении создается небольшой, переиспользуемый модуль loggerfx
, который предоставляет *zap.Logger
.
var Module = fx.Provide(New)
// --snip--
func New() (*zap.Logger, error) {
// --snip--
}
С помощью Fx можно красиво структурировать код. Хендлеры можно расположить во внутрениих каталогах internal/handler/
, например как в hello
хенлер. Все хендлеры разом можно пробросить в настройку Fx через общий модуль, описанный в файле internal/handler/module.go
:
package handler
// --snip--
var Module = fx.Options(
hello.Module,
user.Module,
// ...
)
// In main.go
fx.New(
handler.Module, // this provides all the handlers registered previously
)
Для запуска приложения достаточно запустить go run main.go
.
Fx - очень легковесный DI фреймворк, который способствует правильному структурированию кода. Я пользовался им для создания MVCS приложений. На первый взгляд кажется, что приходится писать больше шаблонного кода, но на практике в коде становится проще ориентироваться. И самое главное - приложение становится проще тестировать.