paint-brush
Principe d'inversion de dépendance dans Go : qu'est-ce que c'est et comment l'utiliserpar@kirooha
27,452 lectures
27,452 lectures

Principe d'inversion de dépendance dans Go : qu'est-ce que c'est et comment l'utiliser

par Kirill Parasotchenko10m2024/05/12
Read on Terminal Reader

Trop long; Pour lire

Dans cet article, nous aborderons le principe d'inversion de dépendance. Bref, nous allons parler de ce que c'est et examiner ce principe en prenant comme exemple une simple application Go.
featured image - Principe d'inversion de dépendance dans Go : qu'est-ce que c'est et comment l'utiliser
Kirill Parasotchenko HackerNoon profile picture

Introduction

Dans cet article, nous aborderons le principe d'inversion de dépendance. Bref, nous allons parler de ce que c'est et examiner ce principe en prenant comme exemple une simple application Go.

Qu'est-ce que le principe d'inversion de dépendance ?

Le principe d'inversion de dépendance (DIP) est l'un des cinq principes SOLID de la programmation orientée objet (POO), introduit pour la première fois par Robert C. Martin. Il est dit:


  1. Les modules de haut niveau ne doivent rien importer des modules de bas niveau. Les deux devraient dépendre d'abstractions (par exemple, des interfaces).


  2. Les abstractions ne devraient pas dépendre de détails. Les détails (implémentations concrètes) devraient dépendre des abstractions.


C'est un principe très connu dans le monde de la conception POO, mais si vous ne l'avez jamais rencontré auparavant, cela peut paraître flou à première vue, alors décomposons ce principe à l'aide d'un exemple précis.

Exemple

Voyons à quoi pourrait ressembler la mise en œuvre du principe DI dans Go. Nous commencerons par un exemple simple d'application HTTP avec un seul point de terminaison/livre, qui renvoie des informations sur un livre en fonction de son identifiant. Pour récupérer des informations sur le livre, l'application interagira avec un service HTTP externe.

Structure du projet

cmd - dossier avec les commandes Go. La fonction principale résidera ici.


interne - dossier avec le code d'application interne. Tout notre code résidera ici.

Exemple de code Spaghetti sans DI

main.go démarre simplement le serveur 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)) }


Voici le code pour gérer notre point de terminaison 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 } }

Comme vous pouvez le voir, actuellement tout le code se trouve directement dans le gestionnaire (à l'exclusion du modèle Book). Dans le gestionnaire, nous créons un client HTTP et faisons une requête à un service externe. Ensuite, nous attribuons un prix au livre. Ici, je pense qu'il est évident pour tout développeur que ce n'est pas la meilleure conception et que le code pour appeler le service externe doit être extrait du gestionnaire. Faisons cela.

La première étape de l'amélioration

Dans un premier temps, déplaçons ce code vers un emplacement séparé. Pour ce faire, nous allons créer un fichier appelé internal/pkg/getbook/usecase.go , où résidera la logique de récupération et de traitement de notre livre, et internal/pkg/getbook/types.go , où nous stockerons le types de getbook nécessaires.


code utilisercase.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 }


code types.go

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


Le nouveau code du gestionnaire :

 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 } }


Comme vous pouvez le voir, le code du gestionnaire est devenu beaucoup plus propre, mais maintenant, il est beaucoup plus intéressant pour nous de jeter un œil à getbook/usecase.go

 type UseCase struct { bookServiceClient *http.Client }


Le UseCase a une dépendance sous la forme de *http.Client, que nous n'initialisons actuellement d'aucune façon. Nous pourrions passer *http.Client dans le constructeur NewUseCase() ou créer *http.Client directement dans le constructeur. Cependant, rappelons encore une fois ce que nous dit le principe DI.


Les modules de haut niveau ne doivent rien importer des modules de bas niveau. Les deux devraient dépendre d'abstractions (par exemple, des interfaces)


Cependant, avec cette approche, nous avons fait exactement le contraire. Notre module de haut niveau, getbook, importe le module de bas niveau, HTTP.

Présentation de l'inversion des dépendances

Réfléchissons à la manière dont nous pourrions résoudre ce problème. Pour commencer, créons un fichier appelé internal/pkg/bookserviceclient/client.go . Ce fichier contiendra l'implémentation des requêtes HTTP vers le service externe et l'interface correspondante.

 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 }


Ensuite, nous devons mettre à jour notre UseCase afin qu'il commence à utiliser l'interface du package 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 }


Il semble que les choses se soient considérablement améliorées et nous avons résolu le problème de dépendance de useсase sur le module de bas niveau. Cependant, nous n’en sommes pas encore là. Allons un peu plus loin. À l'heure actuelle, pour déclarer les dépendances, useсase utilise une interface du module de bas niveau. Pouvons-nous améliorer cela ? Et si nous déclarions les interfaces dont nous avons besoin dans pkg/getbook/types.go ?


De cette façon, nous supprimerions les dépendances explicites sur les modules de bas niveau. Autrement dit, notre module de haut niveau déclarerait toutes les interfaces nécessaires à son fonctionnement, supprimant ainsi toutes les dépendances sur les modules de bas niveau. Au niveau supérieur de l'application ( main.go ), nous implémenterions alors toutes les interfaces nécessaires au fonctionnement de l'usecase.


Rappelons également les types exportés et non exportés dans Go. Devons-nous exporter les interfaces de cas d’utilisation ? Ces interfaces ne sont nécessaires que pour spécifier les dépendances requises par ce package pour son fonctionnement, il est donc préférable de ne pas les exporter.

Code final

cas d'utilisation.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 }


types.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"` }


client.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 }


main.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)) }

Résumé

Dans cet article, nous avons exploré comment implémenter le principe d'inversion de dépendance dans Go. La mise en œuvre de ce principe peut aider à éviter que votre code ne devienne un spaghetti et à le rendre plus facile à maintenir et à lire. Comprendre les dépendances de vos classes et comment les déclarer correctement peut grandement vous simplifier la vie lors du support supplémentaire de votre application.