[Resolver] Golang
Veja neste artigo como aplicar o resolver no Golang
No desenvolvimento de software moderno, o conceito resolver vem ganhando cada vez mais espaço, especialmente quando falamos sobre a flexibilidade e a modularidade necessárias em sistemas distribuídos.
O meu primeiro contato com este padrão foi no desenvolvimento de uma API GraphQL.
Como sabemos, no GraphQL, cada campo de uma query ou mutation é resolvido por uma função chamada de resolver. Essa função é responsável por buscar os dados, transformá-los e entregá-los de forma que o cliente possa utilizá-los, seja através de uma consulta simples ao banco de dados ou uma chamada a um microserviço.
O padrão Resolver foi uma solução natural para o problema de desacoplar a lógica de recuperação de dados da lógica de consulta, permitindo maior flexibilidade na estruturação da API.
Com essa experiência inicial, vi o potencial do padrão em outras linguagens, como o Golang, onde podemos aplicá-lo para resolver chamadas dinâmicas em serviços de API, injeção de dependências ou até mesmo na escolha de rotas em sistemas mais complexos.
Neste artigo, exploraremos como aplicar o padrão resolver no Golang, através de um exemplo prático.
Caso você tenha interesse de clonar a versão final do projeto que será apresentado neste artigo, segue o seu link no meu Github: go-resolver
Analisando este projeto nós temos 3 arquivos:
service.go
package main
import "fmt"
type Service struct{}
func (s *Service) MakeRequest() {
fmt.Println("Fazendo uma requisição!")
}
Aqui nós temos uma serviço simples que contém o método MakeRequest
, que apenas imprime uma mensagem simulando a execução de uma requisição.
resolver.go
package main
import (
"fmt"
"sync"
)
type Resolver struct {
service *Service
once sync.Once
}
func (r *Resolver) ResolveService() *Service {
r.once.Do(func() {
fmt.Println("Inicializando o serviço...")
r.service = &Service{}
})
return r.service
}
Aqui nós temos:
- Uma estrutura simples que é usada para resolver (ou inicializar) o serviço de forma "lazy", ou seja, o serviço só será inicializado quando for realmente necessário, e a inicialização será feita apenas uma vez.
- O campo
once
é do tiposync.Once
, que é uma funcionalidade do Go usada para garantir que um determinado bloco de código seja executado uma única vez, mesmo em ambientes concorrentes (com múltiplas goroutines). - O método
ResolveService
utilizaonce.Do()
para verificar se o serviço já foi inicializado. Se não foi, ele executa o código que inicializa o serviço e atribui a instância ao camposervice
.
main.go
package main
func main() {
resolver := &Resolver{}
service := resolver.ResolveService()
service.MakeRequest()
serviceAgain := resolver.ResolveService()
serviceAgain.MakeRequest()
}
Analisando este arquivo nós temos:
- Criamos uma instância do
Resolver
, que será responsável por inicializar o serviço de forma controlada. - A primeira chamada a
resolver.ResolveService()
inicializa o serviço (já que ele ainda não foi criado) e, em seguida, chama o métodoMakeRequest
, que imprime a mensagem "Fazendo uma requisição!". - A segunda chamada a
resolver.ResolveService()
não inicializa o serviço novamente, pois o serviço já foi instanciado na primeira chamada. Ele apenas retorna a mesma instância já criada, demonstrando o comportamento de inicialização única garantido porsync.Once
.
Executando este código nós temos o seguinte resultado:
Até aqui tranquilo, né? O código demonstra como o Resolver pode ser utilizado para garantir a inicialização controlada e única de um serviço usando o sync.Once
, o que é especialmente útil em cenários onde múltiplas goroutines podem tentar acessar o serviço ao mesmo tempo.
Bom, para ilustrar ainda mais o poder desta padrão, vamos atualizar o nosso código para lidar com fluxos de investimentos.
Para isso, imagine que em vez de inicializar um serviço simples, agora queremos que o Resolver escolha dinamicamente qual tipo de cálculo de investimento será feito com base no tipo de investimento que recebemos como input.
Atualizando a nossa service para que ela simule alguns retornos para diferentes tipos de investimentos.
package main
import "fmt"
type InvestmentService struct{}
func (s *InvestmentService) CalculateInvestment(investmentType string, amount float64) {
switch investmentType {
case "renda_fixa":
fmt.Printf("Calculando investimento em Renda Fixa. Valor final: %.2f\n", amount*1.05)
case "acoes":
fmt.Printf("Calculando investimento em Ações. Valor final: %.2f\n", amount*1.20)
default:
fmt.Println("Tipo de investimento não reconhecido.")
}
}
Agora, o Resolver
que será responsável por inicializar o InvestmentService e garantir que ele seja inicializado apenas uma vez.
package main
import (
"fmt"
"sync"
)
type InvestmentResolver struct {
service *InvestmentService
once sync.Once
}
func (r *InvestmentResolver) ResolveService() *InvestmentService {
r.once.Do(func() {
fmt.Println("Inicializando o serviço de investimento...")
r.service = &InvestmentService{}
})
return r.service
}
por fim, na nossamain.go
, vamos simular o uso do InvestmentResolver para realizar cálculos de diferentes tipos de investimento.
package main
func main() {
resolver := &InvestmentResolver{}
service := resolver.ResolveService()
service.CalculateInvestment("renda_fixa", 1000.0)
serviceAgain := resolver.ResolveService()
serviceAgain.CalculateInvestment("acoes", 1500.0)
}
Executando esse código nós temos:
Note que com esse novo exemplo, nós podemos ver como o padrão Resolver é uma abordagem poderosa para controlar a inicialização e o uso de serviços em sistemas que exigem flexibilidade e modularidade.
Bom, com isso finalizo mais este artigo pessoal.