Pular para o conteúdo principal

Injeção de dependência em Go com fx

A injeção de dependência é um padrão de design de software utilizado por alguns frameworks, como Angular e Spring, que nos permite a inversão de controle entre tipos por meio do uso de uma abstração em vez de uma implementação, o que resulta em um código mais modular e fácil de testar. Em Go, o pacote fx, um sistema de injeção de dependência, permite-nos construir uma aplicação sem a necessidade de  passar objetos para cada chamada de construtor. 

Mas uma estrutura organizada de pastas é importante para facilitar o desenvolvimento de um projeto, e o seguinte código busca mostrar um uso básico de fx e uma estrutura inicial de pastas que serve como exemplo de uso do sistema de injeção de dependência em Go. Tipos de interface não serão criados, uma vez que o objetivo principal aqui é o de exemplificar o uso de fx. 

/cmd
    main.go
/internal
    /app
        app.go
        module.go
    /controller
        controller.go
        module.go
    /usecase
        usecase.go
        module.go

Para cada pasta, há um arquivo module.go para facilitar a configuração de fx, o que irá evitar toda a configuração do sistema de dependência em um único arquivo. Assim, é importante que cada tipo seja configurado em sua respectiva pasta do projeto no arquivo module.go. Em seguida temos o código da configuração da aplicação de fx, em /internal/app.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package app

import (
    "context"

    "github.com/labstack/echo/v4"
    "go.uber.org/fx"
)

func NewServer() *echo.Echo {
    return echo.New()
}

func StartServer(lc fx.Lifecycle, e *echo.Echo) {
    lc.Append(fx.Hook{
        OnStart: func(context.Context) error {
	    go func() {
		if err := e.Start(":1323"); err != nil {
		    e.Logger.Fatal(err)
		}
	    }()
	    return nil
	},
	OnStop: func(ctx context.Context) error {
	    return e.Shutdown(ctx)
	},
    })
}

func StartApp() {
    fx.New(
	fx.Provide(
	    NewServer,
	),
	Modules,
	fx.Invoke(StartServer),
    ).Run()
}

Para requisições HTTP, usamos o framework echo. Do código acima seguem as partes mais importantes:

  • Linha 32: "fx.Provide registra qualquer número de funções usadas como construtor, ensinando a aplicação como instanciar os vários tipos." - da documentação; a chamada da função NewServe, linha 33, intancia *echo.Echo por meio de fx.Provide, que será injetado por fx como argumento da função StartServer, quando chamada por fx.Invoke; 
  • Linha 35: a variável Modules (definida no próximo exemplo de código) são registros de construtores que realizamos nos arquivos module.go de cada pasta do projeto;
  • Linha 36: fx.Invoke registra funções que são executadas assim que a aplicação inicia; "argumentos para estas invocações são construídos usando os construtores registrados por fx.Provide" - da documentação.
Em seguida, temos o código do arquivo module.go em /internal/app/module.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package app

import (
    "github.com/labtrilhadev/go-tutorials/di-fx/internal/controller"
"github.com/labtrilhadev/go-tutorials/di-fx/internal/usecase"
"go.uber.org/fx" ) var Modules = fx.Options( controller.Module, usecase.Module, )

O último trecho de código será o da configuração do controller, pois tem uma proposta de código que acredito ser melhor que configurar todas as rotas da aplicação em um único arquivo, normalmente chamado routes.go. A experiência de uso do Spring Framework mostra que ter a rota configurada no próprio arquivo do controller facilita na identificação do caminho de URLs e nos verbos HTTP utilizados. Segue o código do arquivo /internal/controller/controller.go:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package controller

import (
    "net/http"

    "github.com/labstack/echo/v4"
    "github.com/labtrilhadev/go-tutorials/di-fx/internal/usecase"
) func RegisterAnyController(e *echo.Echo, c AnyController) { e.GET("/", c.Hello) } type AnyController struct { AnyUseCase usecase.AnyUseCase } func NewAnyController(anyUseCase usecase.AnyUseCase) AnyController { return AnyController{anyUseCase} } func (c *AnyController) Hello(ctx echo.Context) error { reqCtx := ctx.Request().Context() return ctx.String(http.StatusOK, c.AnyUseCase.Perform(reqCtx)) }

Seguem notas sobre as linhas mais importantes do código anterior:

  • Linha 10: a função RegisterAnyController tem os seus parâmetros injetados por fx, e permite manter a configuração das rotas de um controller dentro do mesmo arquivo.
  • Linha 18: o construtor NewAnyController será configurado por fx através de fx.Provide, e dessa maneira o framework de injeção de dependência pode injetar a instância do controller como argumento de uma função.
O seguinte código configura fx para chamar RegisterAnyController e NewAnyController no arquivo /internal/controller/module.go:

1
2
3
4
5
6
7
8
package controller

import "go.uber.org/fx"

var Module = fx.Options(
    fx.Provide(NewAnyController),
    fx.Invoke(RegisterAnyController),
)

Do código anterior, a função fx.Provide configura o objeto de AnyController por meio de seu construtor, e a função fx.Invoke, no início da aplicação, já invoca a função RegisterAnyController para configurar as rotas. 

Conclusão

A injeção de dependência nos permite simplificar a passagem de argumentos para construtores e facilita os testes, uma vez que operamos sobre abstrações. O framework fx, após a configuração da construção de objetos de tipos, realiza a passagem de parâmetros para os construtores e para a invocação de métodos. Para visualizar o código do use case, assim como todo o código do tutorial, acesse o repositório no github.   

Comentários

Postagens mais visitadas deste blog

Tratamento de erros em Go

É comum que as aplicações precisem lidar com situações excepcionais, como dados de entrada errados (json mal formatado), falhas de acesso ao disco ou em conexões de rede ou registro inexistente em uma base de dados. A manipulação de erros em Go é diferente de como realizamos em outras linguagens, como Java, Ruby e Python. Nessas linguagens, lidamos com exceções, que tratamos com o uso da estrutura try/catch ou algo similar.  Normalmente as pessoas recorrem à leitura de uma "stack trace" para identificar a linha de um arquivo em que a exceção surgiu, então, ao trabalhar com Go, buscam uma maneira similar através de bibliotecas de log, mas o resultado final são logs em várias partes do código, justamente por não utilizar a maneira própria da linguagem Go para o tratamento de erros. Erros são valores  Em Go não temos uma sintaxe especial para manipular erros, porque erros são valores do tipo error, comumente retornados em uma função. Então, devemos verificar o erro e realizar al

Como criar um ambiente aberto ao debate

Não é raro encontrar pessoas que sejam pressionadas a não expor uma opinião porque estão em um contexto em que cargos têm um maior peso na tomada de decisão que o conhecimento consistente. Não tenho dúvidas que todos confiamos que a um cargo está atrelada a experiência, ou seja, temos uma certa segurança e expectativa no papel que deve ser desempenhado. Porém, problemas podem surgir quando a pessoa errada está em um cargo importante. Um ambiente em que todos - de júnior a sênior - se sentem bem para participar é incapaz de se desenvolver sem o devido valor que merece a busca por conhecimento, capaz de estabelecer uma cultura em que as pessoas envolvidas se sentem convidadas ao debate inteligente, porque não está fundamento em posições, mas na colaboração por meio do saber. Equipes de desenvolvimento, líderes técnicos e de projetos, normalmente, precisam tomar decisões importantes para a vida de um serviço no longo prazo, cujos problemas atuais podem ser frutos de aspectos importantes q