paint-brush
Princípio de inversão de dependência em Go: o que é e como usá-lopor@kirooha
27,528 leituras
27,528 leituras

Princípio de inversão de dependência em Go: o que é e como usá-lo

por Kirill Parasotchenko10m2024/05/12
Read on Terminal Reader

Muito longo; Para ler

Neste artigo, discutiremos o princípio da Inversão de Dependência. Resumindo, falaremos sobre o que é e examinaremos esse princípio usando um aplicativo Go simples como exemplo.
featured image - Princípio de inversão de dependência em Go: o que é e como usá-lo
Kirill Parasotchenko HackerNoon profile picture

Introdução

Neste artigo, discutiremos o princípio da Inversão de Dependência. Resumindo, falaremos sobre o que é e examinaremos esse princípio usando um aplicativo Go simples como exemplo.

Qual é o Princípio da Inversão de Dependência?

O Princípio de Inversão de Dependência (DIP) é um dos cinco princípios SOLID de programação orientada a objetos (OOP), introduzido pela primeira vez por Robert C. Martin. Afirma:


  1. Módulos de alto nível não devem importar nada de módulos de baixo nível. Ambos devem depender de abstrações (por exemplo, interfaces).


  2. As abstrações não devem depender de detalhes. Os detalhes (implementações concretas) devem depender de abstrações.


É um princípio muito conhecido no mundo do design OOP, mas se você nunca o encontrou antes, pode parecer pouco claro à primeira vista, então vamos analisar esse princípio usando um exemplo específico.

Exemplo

Vamos considerar como seria a implementação do princípio DI em Go. Começaremos com um exemplo simples de aplicação HTTP com um único endpoint/livro, que retorna informações sobre um livro com base em seu ID. Para recuperar informações sobre o livro, o aplicativo irá interagir com um serviço HTTP externo.

Estrutura do Projeto

cmd - pasta com comandos Go. A função principal residirá aqui.


interno - pasta com código interno do aplicativo. Todo o nosso código residirá aqui.

Exemplo de código espaguete sem DI

main.go simplesmente inicia o servidor HTTP.

 package main import ( "log" "net/http" "example.com/books/internal/app/httpbookapi" ) func main() { http.Handle("/book", &httpbookapi.Handler{}) log.Print("server listening at 9090") log.Fatal(http.ListenAndServe(":9090", nil)) }


Aqui está o código para lidar com nosso endpoint HTTP:

 package httpbookapi import ( "encoding/json" "fmt" "net/http" "example.com/books/internal/model" ) type Handler struct { } func (h *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var ( ctx = request.Context() id = request.URL.Query().Get("id") book model.Book ) url := fmt.Sprintf("http://localhost:8080/book?id=%s", id) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } resp, err := http.DefaultClient.Do(req) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(&book); err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } book.Price = 10.12 if book.Title == "Pride and Prejudice" { book.Price += 2 } writer.Header().Add("Content-Type", "application/json") if err := json.NewEncoder(writer).Encode(book); err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } }

Como você pode ver, atualmente todo o código está diretamente dentro do manipulador (excluindo o modelo Book). No manipulador, criamos um cliente HTTP e fazemos uma solicitação a um serviço externo. Então atribuímos algum preço ao livro. Aqui, acredito que seja evidente para qualquer desenvolvedor que este não é o melhor design, e o código para chamar o serviço externo precisa ser extraído do manipulador. Vamos fazer isso.

O primeiro passo da melhoria

Como primeiro passo, vamos mover esse código para um local separado. Para fazer isso, criaremos um arquivo chamado internal/pkg/getbook/usecase.go , onde residirá a lógica para recuperar e processar nosso livro, e internal/pkg/getbook/types.go , onde armazenaremos o tipos de getbook necessários.


código usecase.go

 package getbook import ( "context" "encoding/json" "fmt" "net/http" ) type UseCase struct { bookServiceClient *http.Client } func NewUseCase() *UseCase { return &UseCase{} } func (u *UseCase) GetBook(ctx context.Context, id string) (*Book, error) { var ( book Book url = fmt.Sprintf("http://localhost:8080/book?id=%s", id) ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } resp, err := u.bookServiceClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(&book); err != nil { return nil, err } book.Price = 10.12 if book.Title == "Pride and Prejudice" { book.Price += 2 } return &book, nil }


código types.go

 package getbook type Book struct { ID string `json:"id"` Title string `json:"title"` Author string `json:"author"` Price float64 `json:"price"` }


O novo código do manipulador:

 package httpbookapi import ( "encoding/json" "net/http" "example.com/books/internal/pkg/getbook" ) type Handler struct { getBookUseCase *getbook.UseCase } func NewHandler(getBookUseCase *getbook.UseCase) *Handler { return &Handler{ getBookUseCase: getBookUseCase, } } func (h *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var ( ctx = request.Context() id = request.URL.Query().Get("id") ) book, err := h.getBookUseCase.GetBook(ctx, id) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } writer.Header().Add("Content-Type", "application/json") if err := json.NewEncoder(writer).Encode(book); err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } }


Como você pode ver, o código do manipulador ficou muito mais limpo, mas agora é muito mais interessante darmos uma olhada em getbook/usecase.go

 type UseCase struct { bookServiceClient *http.Client }


O UseCase tem uma dependência na forma de *http.Client, que atualmente não estamos inicializando de forma alguma. Poderíamos passar *http.Client para o construtor NewUseCase() ou criar *http.Client diretamente dentro do construtor. No entanto, vamos relembrar mais uma vez o que o princípio DI nos diz.


Módulos de alto nível não devem importar nada de módulos de baixo nível. Ambos devem depender de abstrações (por exemplo, interfaces)


No entanto, com esta abordagem, fizemos exatamente o oposto. Nosso módulo de alto nível, getbook, importa o módulo de baixo nível, HTTP.

Apresentando a inversão de dependência

Vamos pensar em como poderíamos consertar isso. Para começar, vamos criar um arquivo chamado internal/pkg/bookserviceclient/client.go . Este arquivo conterá a implementação das solicitações HTTP ao serviço externo e a interface correspondente.

 package bookserviceclient import ( "context" "fmt" "io" "net/http" ) type Client interface { GetBook(ctx context.Context, id string) ([]byte, error) } type client struct { httpClient *http.Client } func NewClient() Client { return &client{ httpClient: http.DefaultClient, } } func (c *client) GetBook(ctx context.Context, id string) ([]byte, error) { var ( url = fmt.Sprintf("http://localhost:8080/book?id=%s", id) ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return b, nil }


A seguir, precisamos atualizar nosso UseCase para que ele comece a usar a interface do pacote bookserviceclient.

 package getbook import ( "context" "encoding/json" "example.com/books/internal/pkg/bookserviceclient" ) type UseCase struct { bookClient bookserviceclient.Client } func NewUseCase(bookClient bookserviceclient.Client) *Usecase { return &UseCase{ bookClient: bookClient, } } func (u *UseCase) GetBook(ctx context.Context, id string) (*Book, error) { var ( book Book ) b, err := u.bookClient.GetBook(ctx, id) if err != nil { return nil, err } if err := json.Unmarshal(b, &book); err != nil { return nil, err } book.Price = 10.12 if book.Title == "Pride and Prejudice" { book.Price += 2 } return &book, nil }


Parece que as coisas melhoraram significativamente e resolvemos o problema de dependência do useсase no módulo de baixo nível. No entanto, ainda não chegou lá. Vamos dar um passo adiante. No momento, para declarar dependências, useсase está usando uma interface do módulo de baixo nível. Podemos melhorar isso? E se declararmos as interfaces que precisamos em pkg/getbook/types.go ?


Dessa forma, removeríamos dependências explícitas em módulos de baixo nível. Ou seja, nosso módulo de alto nível declararia todas as interfaces necessárias ao seu funcionamento, removendo assim todas as dependências dos módulos de baixo nível. No nível superior do aplicativo ( main.go ), implementaríamos todas as interfaces necessárias para o funcionamento do useсase.


Além disso, vamos relembrar os tipos exportados e não exportados no Go. Precisamos exportar interfaces useсase? Essas interfaces são necessárias apenas para especificar as dependências exigidas por este pacote para seu funcionamento, portanto é melhor não exportá-las.

Código Final

usecase.go

 package getbook import ( "context" "encoding/json" ) type UseCase struct { bookClient bookClient } func NewUseCase(bookClient bookClient) *UseCase { return &UseCase{ bookClient: bookClient, } } func (u *UseCase) GetBook(ctx context.Context, id string) (*Book, error) { var ( book Book ) b, err := u.bookClient.GetBook(ctx, id) if err != nil { return nil, err } if err := json.Unmarshal(b, &book); err != nil { return nil, err } book.Price = 10.12 if book.Title == "Pride and Prejudice" { book.Price += 2 } return &book, nil }


tipos.go

 package getbook import "context" type bookClient interface { GetBook(ctx context.Context, id string) ([]byte, error) } type Book struct { ID string `json:"id"` Title string `json:"title"` Author string `json:"author"` Price float64 `json:"price"` }


cliente.go

 package bookserviceclient import ( "context" "fmt" "io" "net/http" ) type Client struct { httpClient *http.Client } func NewClient() *Client { return &Client{ httpClient: http.DefaultClient, } } func (c *Client) GetBook(ctx context.Context, id string) ([]byte, error) { var ( url = fmt.Sprintf("http://localhost:8080/book?id=%s", id) ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return b, nil }


principal.go

 package main import ( "log" "net/http" "example.com/books/internal/app/httpbookapi" "example.com/books/internal/pkg/bookserviceclient" "example.com/books/internal/pkg/getbook" ) func main() { bookServiceClient := bookserviceclient.NewClient() useCase := getbook.NewUsecase(bookServiceClient) handler := httpbookapi.NewHandler(useCase) http.Handle("/book", handler) log.Print("server listening at 9090") log.Fatal(http.ListenAndServe(":9090", nil)) }

Resumo

Neste artigo, exploramos como implementar o princípio de inversão de dependência em Go. A implementação desse princípio pode ajudar a evitar que seu código se torne um espaguete e torná-lo mais fácil de manter e ler. Compreender as dependências de suas classes e como declará-las corretamente pode simplificar muito sua vida na hora de dar suporte adicional à sua aplicação.