En este artículo, analizaremos el principio de inversión de dependencia. En resumen, hablaremos sobre qué es y examinaremos este principio usando una aplicación Go simple como ejemplo.
El Principio de Inversión de Dependencia (DIP) es uno de los cinco principios SÓLIDOS de la programación orientada a objetos (POO), introducido por primera vez por Robert C. Martin. Afirma:
Los módulos de alto nivel no deben importar nada de los módulos de bajo nivel. Ambos deberían depender de abstracciones (por ejemplo, interfaces).
Las abstracciones no deberían depender de los detalles. Los detalles (implementaciones concretas) deberían depender de abstracciones.
Es un principio muy conocido en el mundo del diseño de programación orientada a objetos, pero si nunca lo ha encontrado antes, puede parecer confuso a primera vista, así que analicemos este principio usando un ejemplo específico.
Consideremos cómo podría verse la implementación del principio DI en Go. Comenzaremos con un ejemplo simple de una aplicación HTTP con un único punto final/libro, que devuelve información sobre un libro según su ID. Para recuperar información sobre el libro, la aplicación interactuará con un servicio HTTP externo.
cmd: carpeta con comandos Go. La función principal residirá aquí.
interno: carpeta con el código interno de la aplicación. Todo nuestro código residirá aquí.
main.go simplemente inicia el 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)) }
Aquí está el código para manejar nuestro punto final 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 puede ver, actualmente todo el código está directamente dentro del controlador (excluyendo el modelo Libro). En el controlador, creamos un cliente HTTP y realizamos una solicitud a un servicio externo. Luego asignamos algún precio al libro. Aquí, creo que es evidente para cualquier desarrollador que este no es el mejor diseño y que el código para llamar al servicio externo debe extraerse del controlador. Vamos a hacer eso.
Como primer paso, movamos este código a un lugar separado. Para hacer esto, crearemos un archivo llamado internal/pkg/getbook/usecase.go , donde residirá la lógica para recuperar y procesar nuestro libro, y internal/pkg/getbook/types.go , donde almacenaremos el tipos de getbook necesarios.
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 tipos.go
package getbook type Book struct { ID string `json:"id"` Title string `json:"title"` Author string `json:"author"` Price float64 `json:"price"` }
El nuevo código de controlador:
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 puede ver, el código del controlador se ha vuelto mucho más limpio, pero ahora es mucho más interesante para nosotros echarle un vistazo a getbook/usecase.go.
type UseCase struct { bookServiceClient *http.Client }
UseCase tiene una dependencia en forma de *http.Client, que actualmente no estamos inicializando de ninguna manera. Podríamos pasar *http.Client al constructor NewUseCase() o crear *http.Client directamente dentro del constructor. Sin embargo, recordemos una vez más lo que nos dice el principio DI.
Los módulos de alto nivel no deben importar nada de los módulos de bajo nivel. Ambos deberían depender de abstracciones (por ejemplo, interfaces)
Sin embargo, con este enfoque, hemos hecho todo lo contrario. Nuestro módulo de alto nivel, getbook, importa el módulo de bajo nivel, HTTP.
Pensemos en cómo podríamos solucionar esto. Para comenzar, creemos un archivo llamado internal/pkg/bookserviceclient/client.go . Este archivo contendrá la implementación de solicitudes HTTP al servicio externo y la interfaz correspondiente.
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 continuación, necesitamos actualizar nuestro UseCase para que comience a usar la interfaz del paquete 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 las cosas han mejorado significativamente y hemos solucionado el problema de dependencia de la utilidad en el módulo de bajo nivel. Sin embargo, aún no ha llegado a ese punto. Vayamos un paso más allá. En este momento, para declarar dependencias, usease está utilizando una interfaz del módulo de bajo nivel. ¿Podemos mejorar esto? ¿Qué pasa si declaramos las interfaces que necesitamos en pkg/getbook/types.go ?
De esta manera, eliminaríamos las dependencias explícitas de los módulos de bajo nivel. Es decir, nuestro módulo de alto nivel declararía todas las interfaces necesarias para su funcionamiento, eliminando así todas las dependencias de los módulos de bajo nivel. En el nivel superior de la aplicación ( main.go ), implementaríamos todas las interfaces necesarias para que usease funcione.
Además, recordemos los tipos exportados y no exportados en Go. ¿Necesitamos exportar interfaces de uso práctico? Estas interfaces sólo son necesarias para especificar las dependencias que requiere este paquete para su funcionamiento, por lo que es mejor no exportarlas.
caso de uso.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)) }
En este artículo, exploramos cómo implementar el principio de inversión de dependencia en Go. Implementar este principio puede ayudar a evitar que su código se convierta en espaguetis y hacerlo más fácil de mantener y leer. Comprender las dependencias de sus clases y cómo declararlas correctamente puede simplificar enormemente su vida a la hora de respaldar aún más su aplicación.