在本文中,我们将讨论依赖倒置原则。简而言之,我们将讨论它是什么,并使用一个简单的 Go 应用程序作为示例来检查这一原则。
依赖倒置原则 (DIP) 是面向对象编程 (OOP) 的五个 SOLID 原则之一,由 Robert C. Martin 首次提出。它指出:
高级模块不应该从低级模块导入任何东西。两者都应该依赖于抽象(例如接口)。
抽象不应该依赖于细节。细节(具体实现)应该依赖于抽象。
这是 OOP 设计领域中非常著名的原则,但如果您以前从未遇到过它,乍一看可能会觉得不清楚,所以让我们用一个具体的例子来分解这个原则。
让我们考虑一下 DI 原则在 Go 中的实现方式。我们将从一个简单的 HTTP 应用程序示例开始,该应用程序具有单个端点/书籍,它根据书籍的 ID 返回有关书籍的信息。为了检索有关书籍的信息,应用程序将与外部 HTTP 服务进行交互。
cmd - 包含 Go 命令的文件夹。主要功能将驻留在此处。
internal - 包含内部应用程序代码的文件夹。我们的所有代码都将驻留在此处。
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 }
types.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 }
接下来,我们需要更新我们的用例,以便它开始使用来自 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 ),我们将实现用例运行所需的所有接口。
另外,我们回顾一下 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"` }
客户端.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 }
主程序
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 中实现依赖倒置原则。实现此原则可以帮助防止代码变得杂乱无章,并使其更易于维护和阅读。了解类的依赖关系以及如何正确声明它们可以大大简化您进一步支持应用程序的工作。