paint-brush
Принцип инверсии зависимостей в Go: что это такое и как его использовать к@kirooha
27,667 чтения
27,667 чтения

Принцип инверсии зависимостей в Go: что это такое и как его использовать

к Kirill Parasotchenko
Kirill Parasotchenko HackerNoon profile picture

Kirill Parasotchenko

@kirooha

Software Engineer (Backend, Golang)

10 мин read2024/05/12
Read on Terminal Reader
Read this story in a terminal
Print this story

Слишком долго; Читать

В этой статье мы обсудим принцип инверсии зависимостей. Вкратце, мы поговорим о том, что это такое, и рассмотрим этот принцип на примере простого приложения Go.
featured image - Принцип инверсии зависимостей в Go: что это такое и как его использовать
Kirill Parasotchenko HackerNoon profile picture
Kirill Parasotchenko

Kirill Parasotchenko

@kirooha

Software Engineer (Backend, Golang)

вступление

В этой статье мы обсудим принцип инверсии зависимостей. Вкратце, мы поговорим о том, что это такое, и рассмотрим этот принцип на примере простого приложения Go.

Что такое принцип инверсии зависимостей?

Принцип инверсии зависимостей (DIP) — один из пяти принципов SOLID объектно-ориентированного программирования (ООП), впервые представленный Робертом К. Мартином. Говорится:


  1. Модули высокого уровня не должны ничего импортировать из модулей низкого уровня. Оба должны зависеть от абстракций (например, интерфейсов).


  2. Абстракции не должны зависеть от деталей. Детали (конкретные реализации) должны зависеть от абстракций.


Это очень известный принцип в мире ООП-проектирования, но если вы никогда раньше с ним не сталкивались, на первый взгляд он может показаться неясным, поэтому давайте разберем этот принцип на конкретном примере.

Пример

Давайте рассмотрим, как могла бы выглядеть реализация принципа DI в Go. Мы начнем с простого примера HTTP-приложения с одной конечной точкой/книгой, которое возвращает информацию о книге на основе ее идентификатора. Чтобы получить информацию о книге, приложение будет взаимодействовать с внешним HTTP-сервисом.

Структура проекта

cmd — папка с командами Go. Основная функция будет находиться здесь.


Internal — папка с внутренним кодом приложения. Весь наш код будет находиться здесь.

image

Пример кода спагетти без DI

main.go просто запускает 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)) }


Вот код для обработки нашей конечной точки 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 } }

Как видите, на данный момент весь код находится непосредственно внутри обработчика (за исключением модели Book). В обработчике создаем HTTP-клиент и делаем запрос к внешнему сервису. Затем мы назначаем книге некоторую цену. Здесь, я думаю, любому разработчику очевидно, что это не лучший дизайн, и код вызова внешнего сервиса нужно извлекать из обработчика. Давайте сделаем это.

Первый шаг улучшения

В качестве первого шага давайте переместим этот код в отдельное место. Для этого мы создадим файл Internal/pkg/getbook/usecase.go , в котором будет находиться логика получения и обработки нашей книги, и файл Internal/pkg/getbook/types.go , где мы будем хранить необходимые типы getbook.


код 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 }


код типов.go

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


Новый код обработчика:

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


Как видите, код обработчика стал намного чище, но теперь нам гораздо интереснее взглянуть на getbook/usecase.go.

 type UseCase struct { bookServiceClient *http.Client }


UseCase имеет зависимость в виде *http.Client, которую мы в настоящее время никак не инициализируем. Мы могли бы передать *http.Client в конструктор NewUseCase() или создать *http.Client непосредственно внутри конструктора. Однако давайте еще раз вспомним, о чем нам говорит принцип DI.


Модули высокого уровня не должны ничего импортировать из модулей низкого уровня. Оба должны зависеть от абстракций (например, интерфейсов).


Однако при таком подходе мы сделали прямо противоположное. Наш модуль высокого уровня getbook импортирует модуль низкого уровня HTTP.

Представляем инверсию зависимостей

Давайте подумаем, как это можно исправить. Для начала давайте создадим файл с именем Internal/pkg/bookserviceclient/client.go . Этот файл будет содержать реализацию HTTP-запросов к внешнему сервису и соответствующий интерфейс.

 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 }


Далее нам нужно обновить наш UseCase, чтобы он начал использовать интерфейс из пакета 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 }


Кажется, ситуация значительно улучшилась, и мы решили проблему зависимости useсase от низкоуровневого модуля. Однако это еще не совсем так. Давайте сделаем еще один шаг вперед. Сейчас для объявления зависимостей useсase использует интерфейс низкоуровневого модуля. Можем ли мы улучшить это? Что, если мы объявим нужные нам интерфейсы в pkg/getbook/types.go ?


Таким образом мы удалим явные зависимости от низкоуровневых модулей. То есть наш высокоуровневый модуль объявил бы все необходимые для его работы интерфейсы, тем самым убрав все зависимости от низкоуровневых модулей. Затем на верхнем уровне приложения ( main.go ) мы реализуем все интерфейсы, необходимые для работы useсase.


Также давайте вспомним экспортируемые и неэкспортируемые типы в Go. Нужно ли экспортировать интерфейсы вариантов использования? Эти интерфейсы нужны только для указания зависимостей, необходимых этому пакету для его работы, поэтому их лучше не экспортировать.

Окончательный код

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 }


типы.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)) }

Краткое содержание

В этой статье мы рассмотрели, как реализовать принцип инверсии зависимостей в Go. Реализация этого принципа может помочь предотвратить превращение вашего кода в спагетти и упростить его поддержку и чтение. Понимание зависимостей ваших классов и того, как их правильно объявлять, может значительно упростить вам жизнь при дальнейшей поддержке вашего приложения.

L O A D I N G
. . . comments & more!

About Author

Kirill Parasotchenko HackerNoon profile picture
Kirill Parasotchenko@kirooha
Software Engineer (Backend, Golang)

БИРКИ

ЭТА СТАТЬЯ БЫЛА ПРЕДСТАВЛЕНА В...

Read on Terminal Reader
Read this story in a terminal
 Terminal
Read this story w/o Javascript
Read this story w/o Javascript
 Lite
X REMOVE AD