paint-brush
Prinzip der Abhängigkeitsumkehrung in Go: Was es ist und wie man es anwendetvon@kirooha
27,512 Lesungen
27,512 Lesungen

Prinzip der Abhängigkeitsumkehrung in Go: Was es ist und wie man es anwendet

von Kirill Parasotchenko10m2024/05/12
Read on Terminal Reader

Zu lang; Lesen

In diesem Artikel werden wir das Prinzip der Abhängigkeitsinversion besprechen. Kurz gesagt werden wir darüber sprechen, was es ist, und dieses Prinzip am Beispiel einer einfachen Go-Anwendung untersuchen.
featured image - Prinzip der Abhängigkeitsumkehrung in Go: Was es ist und wie man es anwendet
Kirill Parasotchenko HackerNoon profile picture

Einleitung

In diesem Artikel werden wir das Prinzip der Abhängigkeitsinversion besprechen. Kurz gesagt werden wir darüber sprechen, was es ist, und dieses Prinzip am Beispiel einer einfachen Go-Anwendung untersuchen.

Was ist das Abhängigkeitsumkehrprinzip?

Das Dependency Inversion Principle (DIP) ist eines der fünf SOLID-Prinzipien der objektorientierten Programmierung (OOP), das erstmals von Robert C. Martin vorgestellt wurde. Es besagt:


  1. Module auf höherer Ebene sollten nichts aus Modulen auf niedriger Ebene importieren. Beide sollten von Abstraktionen abhängen (z. B. Schnittstellen).


  2. Abstraktionen sollten nicht von Details abhängen. Details (konkrete Implementierungen) sollten von Abstraktionen abhängen.


Es handelt sich um ein sehr bekanntes Prinzip in der Welt des OOP-Designs. Wenn Sie jedoch noch nie damit in Berührung gekommen sind, erscheint es auf den ersten Blick möglicherweise unklar. Lassen Sie uns das Prinzip daher anhand eines konkreten Beispiels näher erläutern.

Beispiel

Betrachten wir, wie die Implementierung des DI-Prinzips in Go aussehen könnte. Wir beginnen mit einem einfachen Beispiel einer HTTP-Anwendung mit einem einzigen Endpunkt/Buch, die Informationen über ein Buch basierend auf seiner ID zurückgibt. Um Informationen über das Buch abzurufen, interagiert die Anwendung mit einem externen HTTP-Dienst.

Projektstruktur

cmd - Ordner mit Go-Befehlen. Die Hauptfunktion befindet sich hier.


intern - Ordner mit internem Anwendungscode. Unser gesamter Code befindet sich hier.

Beispiel für Spaghetticode ohne DI

main.go startet einfach den HTTP-Server.

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


Hier ist der Code zur Handhabung unseres HTTP-Endpunkts:

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

Wie Sie sehen, befindet sich derzeit der gesamte Code direkt im Handler (mit Ausnahme des Buchmodells). Im Handler erstellen wir einen HTTP-Client und stellen eine Anfrage an einen externen Dienst. Dann weisen wir dem Buch einen Preis zu. Ich glaube, es ist für jeden Entwickler offensichtlich, dass dies nicht das beste Design ist und der Code zum Aufrufen des externen Dienstes aus dem Handler extrahiert werden muss. Lassen Sie uns das tun.

Der erste Schritt zur Verbesserung

Lassen Sie uns als ersten Schritt diesen Code an einen anderen Ort verschieben. Dazu erstellen wir eine Datei namens internal/pkg/getbook/usecase.go , in der die Logik zum Abrufen und Verarbeiten unseres Buchs gespeichert wird, und internal/pkg/getbook/types.go , in der wir die erforderlichen Getbook-Typen speichern.


usecase.go-Code

 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 }


Typen.go-Code

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


Der neue Handlercode:

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


Wie Sie sehen, ist der Handler-Code viel sauberer geworden, aber jetzt ist es für uns viel interessanter, einen Blick auf getbook/usecase.go zu werfen

 type UseCase struct { bookServiceClient *http.Client }


Der UseCase hat eine Abhängigkeit in Form von *http.Client, die wir derzeit in keiner Weise initialisieren. Wir könnten *http.Client an den NewUseCase()-Konstruktor übergeben oder *http.Client direkt im Konstruktor erstellen. Erinnern wir uns jedoch noch einmal daran, was uns das DI-Prinzip sagt.


Module auf höherer Ebene sollten nichts aus Modulen auf niedriger Ebene importieren. Beide sollten von Abstraktionen abhängen (z. B. Schnittstellen).


Mit diesem Ansatz haben wir jedoch genau das Gegenteil erreicht. Unser High-Level-Modul getbook importiert das Low-Level-Modul HTTP.

Einführung in die Abhängigkeitsumkehr

Lassen Sie uns darüber nachdenken, wie wir das beheben könnten. Erstellen wir zunächst eine Datei namens internal/pkg/bookserviceclient/client.go . Diese Datei enthält die Implementierung von HTTP-Anfragen an den externen Dienst und die entsprechende Schnittstelle.

 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 }


Als Nächstes müssen wir unseren UseCase aktualisieren, damit er die Schnittstelle aus dem Bookserviceclient-Paket verwendet.

 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 }


Es scheint, als ob sich die Dinge deutlich verbessert haben und wir das Abhängigkeitsproblem von useсase im Low-Level-Modul gelöst haben. Allerdings ist es noch nicht ganz so weit. Gehen wir einen Schritt weiter. Im Moment verwendet useсase zum Deklarieren von Abhängigkeiten eine Schnittstelle aus dem Low-Level-Modul. Können wir das verbessern? Was wäre, wenn wir die benötigten Schnittstellen in pkg/getbook/types.go deklarieren würden?


Auf diese Weise würden wir explizite Abhängigkeiten von Modulen auf niedriger Ebene entfernen. Das heißt, unser Modul auf hoher Ebene würde alle für seinen Betrieb erforderlichen Schnittstellen deklarieren und so alle Abhängigkeiten von Modulen auf niedriger Ebene entfernen. Auf der obersten Ebene der Anwendung ( main.go ) würden wir dann alle Schnittstellen implementieren, die für die Funktion von useсase erforderlich sind.


Erinnern wir uns auch an exportierte und nicht exportierte Typen in Go. Müssen wir Usecase-Schnittstellen exportieren? Diese Schnittstellen werden nur benötigt, um die Abhängigkeiten anzugeben, die dieses Paket für seinen Betrieb benötigt. Daher ist es besser, sie nicht zu exportieren.

Endgültiger Code

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


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


Kunde.gehen

 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 }


Hauptnavigation

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

Zusammenfassung

In diesem Artikel haben wir untersucht, wie man das Prinzip der Abhängigkeitsumkehr in Go implementiert. Die Implementierung dieses Prinzips kann dazu beitragen, dass Ihr Code nicht zu Spaghetti wird, und ihn leichter wartbar und lesbar machen. Wenn Sie die Abhängigkeiten Ihrer Klassen verstehen und wissen, wie Sie sie richtig deklarieren, kann Ihnen das Leben bei der weiteren Unterstützung Ihrer Anwendung erheblich leichter fallen.