Injeção de dependência em Go
Erick Takeshi
Posted on February 28, 2024
Software é uma arte em constante mudança e evolução.
Como programadores, sabemos que certamente seremos requisitados a mudarmos nosso código, seja implementando uma feature nova, corrigindo um bug ou rodando em um ambiente novo. Tendo isso em mente, uma das maiores características que devemos mirar para que nosso código tenha é a de ser fácil de modificar.
Engenheiros de software estão sempre repetindo palavras como desacoplamento, que significa que diferentes partes do software podem ser modificadas sem que afetem outras partes.
Uma das técnicas mais famosas, quando se trata de alcançar desacoplamento, é a injeção de dependência. A injeção de dependência é um conceito que dita que seu código deve ser explícito ao declarar suas dependências, sendo este conceito cunhado pela lenda Uncle Bob (Robert Martin), no seu famigerado artigo “The Dependency Inversion Principle”, datado de 1996.
Implementando em Go
Em Go, temos duas construções importantes, sendo structs (para definir objetos, ou seja, agrupamentos de dados correlacionados) e interfaces (para definir comportamentos dos objetos). Interfaces em Go usam um mecanismo de resolução implícito, ou seja, não é necessário declarar explicitamente que algum tipo implementa uma interface específica, isso é feito de maneira implícita, algo análogo a duck typing em outras linguagens. Interfaces implícitas em Go fazem com que injeção de dependência seja implementada de maneira simples.
Algumas técnicas simples de implementar de injeção de dependência em Go são:
- Injeção por constructor
- Injeção por propriedades (setter methods)
Injeção por constructor
Em injeção por constructor, normalmente, temos uma função factory (construtora) que vai retornar uma instância de uma struct com suas dependências já associadas.
Abaixo um exemplo de código em Go.
type Repository interface{
// repo methods
}
type AppLogger interface{
Log(message string)
}
type UseCase struct {
logger AppLogger
repository Repository
}
func NewUseCase(logger AppLogger, repository Repository) *UserCase {
return &UseCase{
logger: logger,
repository: repository
}
}
func (u *UseCase) Call(ctx context.Context) error {
// do some stuff
return nil
}
// injetando as dependências
func main() {
// instancias concretas
logger := MyLogger{}
repo := MyRepository{}
useCase := NewUseCase(logger, repo)
useCase.Call()
// ... do something
}
Um ponto importante a se destacas é que com injeção por constructor estamos fixando as dependências em tempo de compilação, ou seja, não podemos trocas elas dinamicamente conforme alguma condição. Existem vantagens e desvantagens nesse método.
Injeção por propriedades
Na injeção por propriedades utilizamos um método setter para alocar a dependência correta, assim sendo mais flexível, podendo trocar a dependência em tempo de runtime.
Segue o exemplo para ilustrar:
type UseCase struct {
logger AppLogger
repository Repository
}
func (u *UseCase) SetLogger(logger AppLogger) {
u.logger = logger
}
func (u *UseCase) SetRepository(repo Repository) {
u.repository = repo
}
// injetando as dependências
func main() {
// instancias concretas
logger := MyLogger{}
repo := MyRepo{}
useCase := UseCase{}
useCase.SetLogger(logger)
useCase.SetRepository(repo)
useCase.Call()
// ... do something
}
Injeção manual vs via framework
No exemplos apresentados estamos utilizando a técnica de injeção manual, ou seja, estamos utilizando ferramentas nativas da linguagem, sem utilizar nenhuma dependência externa, como libs ou frameworks.
Existem alguns frameworks muito utilizados em Go, como o Wire do Google ou o Dig da Uber.
Cada framework possui suas vantagens e desvantagens, além disso, cabe ainda ponderar se faz sentido o uso de um framework ou não.
Injeção Manual:
Prós:
- Controle completo de como as dependências são injetadas
- Nenhuma dependência externa
- Muito mais simples de entender e não requer nenhum conhecimento extra, como DSL própria
Contras:
- Requer escrita de mais código boilerplate
- Pode ser muito passivo de erro humano, principalmente quando as dependências começam a ficar mais complexas, conforme o projeto cresce
- Acaba sendo muito complexo de manter grafos complexos de dependências
Framework
Prós:
- Reduz boilerplate provendo um mecanismo automático
- Ajuda a promover melhores práticas e padrões de design para orquestrar dependências
- Pode ajudar a simplificar testes, ao facilitar o uso de mock objects
Contras:
- Adiciona uma dependência externa ao projeto
- Requer o aprendizado de um framework novo, que envolve conceitos, configurações e um DSL
- Pode não ser apropriado para todos os projetos, as vezes pode adicionar problemas de uso de memória ou performance que são cruciais para alguns projetos.
Em linhas gerais, injeção manual ajuda a termos mais controle sobre o processo, porém requer mais código e pode ser mais suscetível a erros, enquanto que um framework automatiza muito desse processo e promove melhores práticas, ao custo de aumentar curva de aprendizado, adicionar uma dependência externa e talvez sacrificar um pouco de performance (como alguns frameworks utilizam de Reflection).
A escolha vai depender de diversos fatores, como a complexidade do projeto, a familiaridade do time com conceitos de injeção de dependência e os trade-offs entre controle vs conveniência.
tl;dr
Go é uma linguagem poderosa que nos permite implementar injeção de dependência de maneira simples, sem a necessidade de frameworks, como frequentemente exigido em linguagens como Java. No entanto, também temos à disposição frameworks em Go, como o Wire da Google, que automatizam parte desse processo.
A decisão de utilizar um framework ou não deve ser cuidadosamente considerada, avaliando as necessidades específicas de cada projeto e os trade-offs entre simplicidade e controle.
Posted on February 28, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.