關注點分離、鬆散耦合系統和依賴倒置原則等都是軟體工程中眾所周知的概念,並且在建立良好的計算機程式的過程中非常重要。在本文中,我們將討論一種完全應用這三個原則的技術,稱為依賴注入。
Wire是 Go 中用於依賴注入的程式碼生成器。Wire 會為我們生成必要的初始化程式碼。我們只需要定義提供者和注入者。提供者是普通的 Go 函式,根據給定的依賴關係提供值,而注入器是按依賴順序呼叫提供者的函式。
案例:
考慮一下,我們正在開發為使用者註冊提供端點的 HTTP 伺服器。儘管只有一個端點,但它設計為三層,通常出現在更復雜的應用程式中:儲存庫、用例和控制器。
假設有以下目錄結構,
. ├── go.modgo.mod ├── go.sum ├── internal │ ├── domain │ │ ├── model │ │ │ └── user.go │ │ └── repository │ │ └── user.go │ ├── handler │ │ └── handler.go │ ├── interface │ │ └── datastore │ │ └── user.go │ └── usecase │ ├── request │ │ └── user.go │ ├── user │ │ └── user.go │ └── user.go └── main.go
|
現在,讓我們在 internal/interface/datastore/user.go 中定義第一個提供程式。在下面的程式碼片段中,New 是一個提供程式函式,它將 *sql.DB 作為依賴關係,並返回 Repository 的具體實現。
<font>// internal/interface/datastore/user.go<i> package datastore
import ( "context" "database/sql" "inject/internal/domain/model" )
type Repository struct { db *sql.DB }
func New(db *sql.DB) *Repository { return &Repository{db: db} }
func (r Repository) Create(ctx context.Context, user model.User) error { // TODO: implement me<i> return nil }
|
Usecase 層將透過抽象或介面使用 Repository 的具體實現。換句話說,我們為 Usecase 層提供的功能依賴於介面,而不是 Repository 的具體實現。
從技術上講,這個介面應由消費層擁有,但我個人認為,這並不一定意味著它們都應存在於同一個包中。在我們的例子中,Usecase 的提供者和 Repository 的介面分別定義在 internal/usecase/user/user.go 和 internal/domain/repository/user.go 中。
<font>// internal/usecase/user/user.go<i> package user
import ( "context" "inject/internal/domain/repository" "inject/internal/usecase/request" )
type Usecase struct { repository repository.Repository }
func New(repository repository.Repository) *Usecase { return &Usecase{repository: repository} }
func (u Usecase) Create(ctx context.Context, req request.CreateUserRequest) error { // TODO: implement me<i> return nil }
|
與前面的 Repository 提供程式一樣,我們這裡的 Usecase 提供程式也會返回一個具體實現。
<font>// internal/domain/repository/user.go<i> package repository
import ( "context" "inject/internal/domain/model" )
type Repository interface { Create(ctx context.Context, user model.User) error }
|
最後,Usecase 的具體實現也將透過抽象或介面被 Controller 使用。Controller 的提供程式和 Usecase 的介面分別在 internal/handler/handler.go 和 internal/usecase/user.go 中定義,具體如下
<font>// internal/interface/datastore/user.go<i> package handler
import ( "inject/internal/usecase" "net/http" )
type Handler struct { usecase usecase.Usecase }
func New(usecase usecase.Usecase) *Handler { return &Handler{usecase: usecase} }
func (h Handler) Create() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // TODO: implement me<i> w.WriteHeader(http.StatusOK) } }
// internal/usecase/user.go<i> package usecase
import ( "context" "inject/internal/usecase/request" )
type Usecase interface { Create(ctx context.Context, req request.CreateUserRequest) error }
|
現在,所有必要的提供程式都已完成,我們可以在 main.go 中手動執行依賴注入,如下所示
<font>// main.go<i> package main
import ( "database/sql" "log" "net/http"
"inject/internal/handler" "inject/internal/interface/datastore" "inject/internal/usecase/user"
_ "github.com/go-sql-driver/mysql" )
func main() { db, err := sql.Open("mysql", "dataSourceName") if err != nil { log.Fatalf("sql.Open: %v", err) }
repository := datastore.New(db) usecase := user.New(repository) handler := handler.New(usecase)
mux := http.NewServeMux() mux.HandleFunc("POST /users", handler.Create())
log.Fatalf("http.ListenAndServe: %v", http.ListenAndServe(":8000", mux)) }
|
接下來,如何使用 Wire 生成類似上述的初始化程式碼?
使用 Wire
透過 Wire,我們打算讓最終的 main.go 看起來更簡潔,就像這樣
<font>// main.go<i> package main
import ( "log" "net/http" )
func main() { handler, err := InitializeHandler() if err != nil { log.Fatal(err) }
log.Fatal(http.ListenAndServe(":8000", handler)) }
|
我們可以先建立一個檔案,通常命名為 wire.go。它可以定義在一個單獨的軟體包中,但在本例中,我們將把它定義在專案的根目錄下。但在建立 wire.go 之前,最好先重構一下之前的部分程式碼,尤其是在建立資料庫連線例項和註冊 API 路由時。下面的新提供程式就能實現這一目的、
<font>// pkg/mysql/mysql.go<i> package mysql
import ( "database/sql"
_ "github.com/go-sql-driver/mysql" )
func New() (*sql.DB, error) { db, err := sql.Open("mysql", "dataSourceName") if err != nil { return nil, err } return db, nil }
// internal/handler/route.go<i> package handler
import "net/http"
func Register(handler *Handler) *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("POST /users", handler.Create()) return mux }
|
上述提供者函式 Register 接受處理程式的具體實現。當然,也可以使用抽象或介面。但我們將保持原樣,就像我們讓 Repository 的提供函式接受 *sql.DB 型別的具體實現一樣。這與前面提到的依賴反轉原則並不矛盾。事實上,這也許是一個很好的例子,說明如果沒有直接原因,我們不必在程式碼中建立抽象。
好了,讓我們回到我們的 wire.go。根據我們簡化後的 main.go 檔案,你可能已經意識到 InitializeHandler 函式可能是由 Wire 生成的--是的,沒錯!為了正確生成這個函式,我們可以這樣編寫 wire.go
<font>//go:build wireinject<i> // +build wireinject<i>
package main
import ( "net/http" "inject/internal/domain/repository" "inject/internal/handler" "inject/internal/interface/datastore" "inject/internal/usecase" "inject/internal/usecase/user" "inject/pkg/mysql"
"github.com/google/wire" )
func InitializeHandler() (*http.ServeMux, error) { wire.Build( mysql.New, datastore.New, wire.Bind(new(repository.Repository), new(*datastore.Repository)), user.New, wire.Bind(new(usecase.Usecase), new(*user.Usecase)), handler.New, handler.Register, ) return &http.ServeMux{}, nil }
|
基本上,在 wire.go 中,我們會告訴 Wire 有關注入函式 InitializeHandler 的模板。它返回 *http.ServeMux 和錯誤。請注意,返回值 (&http.ServeMux{}, nil) 只是為了滿足編譯器的要求。為了正確返回所需值,我們在 Build 函式中宣告瞭所有必要的提供程式:mysql.New、datastore.New、user.New、handler.New 和 handler.Register。
儘管 Wire 足夠聰明,能識別出依賴關係圖,但它仍然需要被明確告知某些具體實現滿足某些介面。請記住,datastore.New 和 user.New 返回的具體實現型別為 *datastore.Repository 和 *user.Usecase,它們分別滿足 repository.Repository 和 usecase.Usecase 介面。這兩種情況所需的顯式宣告都是透過 Bind 函式實現的。
請注意,我們需要將 wire.go 排除在最終二進位制檔案之外。這可以透過在 wire.go 檔案頂部新增構建約束來實現。
接下來,我們可以在應用程式的根目錄下呼叫 wire 命令
如果之前沒有安裝 Wire,請先執行以下命令
go install github.com/google/wire/cmd/wire@latest
|
該 wire 命令將生成一個名為 wire_gen.go 的檔案,其內容是 InitializeHandler 函式的生成程式碼,如下所示
<font>// Code generated by Wire. DO NOT EDIT.<i>
//go:generate go run -mod=mod github.com/google/wire/cmd/wire<i> //go:build !wireinject<i> // +build !wireinject<i>
package wire
import ( "inject/internal/handler" "inject/internal/interface/datastore" "inject/internal/usecase/user" "inject/pkg/mysql" "net/http" )
// Injectors from wire.go:<i>
//go:generate wire<i> func InitializeHandler() (*http.ServeMux, error) { db, err := mysql.New() if err != nil { return nil, err } repository := datastore.New(db) usecase := user.New(repository) handlerHandler := handler.New(usecase) serveMux := handler.Register(handlerHandler) return serveMux, nil }
|
生成的初始化器程式碼與我們在第一版 main.go 中編寫的程式碼基本相同。
修改依賴關係
比方說,我們想修改 mysql.New 提供程式以接受配置結構體,因為我們不想直接在其中硬編碼資料來源名稱--這種做法通常被認為是不好的。為此,我們建立了一個特殊目錄來儲存應用程式配置檔案,並建立了一個新的提供程式來讀取檔案並返回 config 結構。我們最終的目錄結構將如下所示:
. ├── config │ ├── config.gogo │ └── file │ └── config.json ├── go.mod ├── go.sum ├── internal │ ├── domain │ │ ├── model │ │ │ └── user.go │ │ └── repository │ │ └── user.go │ ├── handler │ │ ├── handler.go │ │ └── route.go │ ├── interface │ │ └── datastore │ │ └── user.go │ └── usecase │ ├── request │ │ └── user.go │ ├── user │ │ └── user.go │ └── user.go ├── main.go ├── pkg │ └── mysql │ └── mysql.go ├── wire_gen.go └── wire.go
|
在 config/config.go 中,我們定義了 Config 結構及其提供者:
package config
type Config struct { DatabaseDSN string AppPort string }
func Load() (Config, error) { <font>// TODO: implement me<i> return Config{}, nil }
|
接下來,我們要做的就是把這個新的提供程式新增到 wire.go 檔案中。是的,你說得沒錯,把它作為 Build 函式管道的一部分插入、
<font>//go:build wireinject<i> // +build wireinject<i>
package wire
import ( "net/http"
"inject/config" "inject/internal/domain/repository" "inject/internal/handler" "inject/internal/interface/datastore" "inject/internal/usecase" "inject/internal/usecase/user" "inject/pkg/mysql"
"github.com/google/wire" )
func InitializeHandler() (*http.ServeMux, error) { wire.Build( config.Load, mysql.New, datastore.New, wire.Bind(new(repository.Repository), new(*datastore.Repository)), user.New, wire.Bind(new(usecase.Usecase), new(*user.Usecase)), handler.New, handler.Register, ) return &http.ServeMux{}, nil }
|
再次重新執行 wire 命令--或者這次我們也可以執行 go 生成命令--將告訴 Wire 重新生成初始化程式碼,結果如下
<font>// Code generated by Wire. DO NOT EDIT.<i>
//go:generate go run -mod=mod github.com/google/wire/cmd/wire<i> //go:build !wireinject<i> // +build !wireinject<i>
package wire
import ( "inject/config" "inject/internal/handler" "inject/internal/interface/datastore" "inject/internal/usecase/user" "inject/pkg/mysql" "net/http" )
// Injectors from wire.go:<i>
func InitializeHandler() (*http.ServeMux, error) { configConfig, err := config.Load() if err != nil { return nil, err } db, err := mysql.New(configConfig) if err != nil { return nil, err } repository := datastore.New(db) usecase := user.New(repository) handlerHandler := handler.New(usecase) serveMux := handler.Register(handlerHandler) return serveMux, nil }
|
總結
我們介紹了使用 Wire 的一個簡單示例,演示了它如何幫助我們透過依賴注入來構建初始化程式碼。但這並不是 Wire 的全部。事實上,它還有一些其他有用的功能尚未在此討論。要想最大限度地利用 Wire,請查閱此處的文件here.。