電子書推送網(wǎng)站怎么做會(huì)計(jì)培訓(xùn)班
一、前言
線上項(xiàng)目往往依賴非常多的具備特定能力的資源,如:DB、MQ、各種中間件,以及隨著項(xiàng)目業(yè)務(wù)的復(fù)雜化,單一項(xiàng)目?jī)?nèi),業(yè)務(wù)模塊也逐漸增多,如何高效、整潔管理各種資源十分重要。
本文從“術(shù)”層面,講述“依賴注入”的實(shí)現(xiàn),帶你體會(huì)其對(duì)于整潔架構(gòu) & DDD 等設(shè)計(jì)思想的落地,起到的支撐作用。
涉及內(nèi)容:
-
最熱門的 golang 依賴注入庫,GitHub 🌟 12.5k:https://github.com/google/wire
-
GiuHub 🌟 22.5k 的 golang 微服務(wù)框架 kratos 默認(rèn)使用 wire 作為依賴注入方式:https://github.com/go-kratos/kratos
-
Spring Boot 與 Golang 的依賴注入對(duì)比
-
依賴注入的設(shè)計(jì)哲學(xué)
📺 B站賬號(hào):白澤talk,絕大部分博客內(nèi)容都將會(huì)通過視頻講解,不過文章一般是先于視頻發(fā)布
白澤的開源 Golang 學(xué)習(xí)倉庫:https://github.com/BaiZe1998/go-learning,用于文章歸檔 & 聚合博客代碼案例
公眾號(hào)【白澤talk】,本期內(nèi)容的 pdf 版本,可以關(guān)注公眾號(hào),回復(fù)【依賴注入】獲得,往期資源的獲取,都是類似的方式。
二、What
📒 本文所涉及編寫的代碼,已收錄于 https://github.com/BaiZe1998/go-learning/di 目錄
一句話概括:實(shí)例 A 的創(chuàng)建,依賴于實(shí)例 B 的創(chuàng)建,且在實(shí)例 A 的生命周期內(nèi),持有對(duì)實(shí)例 B 的訪問權(quán)限。
2.1 案例分析
依賴注入(Dependency Injection, DI),以 Golang 為例,左側(cè)為手動(dòng)完成依賴注入,右側(cè)為不使用依賴注入:
🌟 不使用依賴注入風(fēng)險(xiǎn):
- 全局變量十分不安全,存在覆寫的可能
- 資源散落在各處,可能重復(fù)創(chuàng)建,浪費(fèi)內(nèi)存,后續(xù)維護(hù)能力極差
- 提高循環(huán)依賴的風(fēng)險(xiǎn)
- 全局變量的引入提高單元測(cè)試的成本
- 不使用依賴注入 demo
package mainvar (mysqlUrl = "mysql://blabla"// 全局?jǐn)?shù)據(jù)庫實(shí)例db = NewMySQLClient(mysqlUrl)
)func NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}type MySQLClient struct {url string
}func (c *MySQLClient) Exec(query string, args ...interface{}) string {return "data"
}func NewApp() *App {return &App{}
}type App struct {
}func (a *App) GetData(query string, args ...interface{}) string {data := db.Exec(query, args...)return data
}// 不使用依賴注入
func main() {app := NewApp()rest := app.GetData("select * from table where id = ?", "1")println(rest)
}
- 手動(dòng)依賴注入 demo
package mainfunc NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}type MySQLClient struct {url string
}func (c *MySQLClient) Exec(query string, args ...interface{}) string {return "data"
}func NewApp(client *MySQLClient) *App {return &App{client: client}
}type App struct {// App 持有唯一的 MySQLClient 實(shí)例client *MySQLClient
}func (a *App) GetData(query string, args ...interface{}) string {data := a.client.Exec(query, args...)return data
}// 手動(dòng)依賴注入
func main() {client := NewMySQLClient("mysql://blabla")app := NewApp(client)rest := app.GetData("select * from table where id = ?", "1")println(rest)
}
三、Why
依賴注入 (Dependency Injection,縮寫為 DI),可以理解為一種代碼的構(gòu)造模式(就是寫法),按照這樣的方式來寫,能夠讓你的代碼更加容易維護(hù)。
四、How
4.1 Golang 依賴注入
以 Golang 🌟 最多的開源庫 wire 為例講解:https://github.com/google/wire/blob/main/docs/guide.md
wire是由 google 開源的一個(gè)供 Go 語言使用的依賴注入代碼生成工具。它能夠根據(jù)你的代碼,生成相應(yīng)的依賴注入 go 代碼。
而與其它依靠反射實(shí)現(xiàn)的依賴注入工具不同的是,wire 能在編譯期(準(zhǔn)確地說是代碼生成時(shí))如果依賴注入有問題,在代碼生成時(shí)即可報(bào)出來,不會(huì)拖到運(yùn)行時(shí)才報(bào),更便于 debug。
- Install:
go install github.com/google/wire/cmd/wire@latest
- provider: a function that can produce a value
以上面手動(dòng)實(shí)現(xiàn)依賴注入為基礎(chǔ),wire 做的工作是幫助開發(fā)者完成如下組裝過程
client := NewMySQLClient("mysql://blabla")
app := NewApp(client)
而其中用到的 NewMySQLClient、NewApp 在 wire 定義為一個(gè)個(gè)的 provider,是需要提前由開發(fā)者實(shí)現(xiàn)的。
func NewMySQLClient(url string) *MySQLClient {return &MySQLClient{url: url}
}func NewApp(client *MySQLClient) *App {return &App{client: client}
}
假設(shè)系統(tǒng)中的資源很多,配置很多,出現(xiàn)了如下復(fù)雜的初始化流程,人工完成依賴注入則變得復(fù)雜:
a := NewA(xxx, yyy) error
b := NewB(ctx, a) error
c := NewC(zzz, a, b) error
d := NewD(www, kkk, a) error
e := NewD(ctx, b, d) error
- injector: a function that calls providers in dependency order
如下是名為 wire.go 的依賴注入配置文件,是一個(gè)只會(huì)被 wire 命令行工具處理的 injector 文件,用于聲明依賴注入流程。
wire.go:
//go:build wireinject
// +build wireinject// The build tag makes sure the stub is not built in the final build.package mainimport "github.com/google/wire"// wireApp init application.
func wireApp(url string) *App {wire.Build(NewMySQLClient, NewApp)return nil
}
執(zhí)行 wire
命令,則在當(dāng)前目錄下生成 wire_gen.go 文件,此時(shí)的 wireApp 函數(shù),就等價(jià)于最初手動(dòng)編寫的依賴注入流程,可以在真正需要初始化的引入。
wire_gen.go:
// Code generated by Wire. DO NOT EDIT.//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinjectpackage main// Injectors from wire.go:// wireApp init application.
func wireApp(url string) *App {mySQLClient := NewMySQLClient(url)app := NewApp(mySQLClient)return app
}
4.2 針對(duì)復(fù)雜項(xiàng)目的依賴注入設(shè)計(jì)哲學(xué)
這里以 go-kratos 的模版項(xiàng)目為例講解,是一個(gè) helloworld 服務(wù),我們著重分析其借助 wire 進(jìn)行依賴注入的部分。
以下 helloworld 模板服務(wù)的 interanl 目錄的內(nèi)容:
.
├── biz
│ ├── README.md
│ ├── biz.go
│ └── greeter.go
├── conf
│ ├── conf.pb.go
│ └── conf.proto
├── data
│ ├── README.md
│ ├── data.go
│ └── greeter.go
├── server
│ ├── grpc.go
│ ├── http.go
│ └── server.go
└── service├── README.md├── greeter.go└── service.go
各個(gè)目錄的關(guān)系如圖:
-
data:業(yè)務(wù)數(shù)據(jù)訪問,包含 cache、db 等封裝,實(shí)現(xiàn)了 biz 的 repo 接口,data 偏重業(yè)務(wù)的含義,它所要做的是將領(lǐng)域?qū)ο笾匦履贸鰜怼?/p>
-
biz:業(yè)務(wù)邏輯的組裝層,類似 DDD 的 domain 層,data 類似 DDD 的 repo,repo 接口在這里定義,使用依賴倒置的原則。
-
service:實(shí)現(xiàn)了 api 定義的服務(wù)層,類似 DDD 的 application 層,處理 DTO 到 biz 領(lǐng)域?qū)嶓w的轉(zhuǎn)換(DTO -> DO),同時(shí)協(xié)同各類 biz 交互,但是不應(yīng)處理復(fù)雜邏輯。
-
server:為http和grpc實(shí)例的創(chuàng)建和配置,以及注冊(cè)對(duì)應(yīng)的 service 。
🌟上圖右側(cè)部分,表示了模塊之間的依賴關(guān)系,可以看到,依賴的注入是逆向的,資源往往被業(yè)務(wù)模塊持有,業(yè)務(wù)模塊則被負(fù)責(zé)編排業(yè)務(wù)的應(yīng)用持有,應(yīng)用則被負(fù)責(zé)對(duì)外通信的模塊持有。
此時(shí)在服務(wù)啟動(dòng)前的實(shí)例化階段,provider 的定義和注入,本質(zhì)是這樣一種狀態(tài):
func main() {dbClient := NewDBClient()dataN := NewDataN(dbClient)dataM := NewDataM(dbClient)bizA := NewBizA(dataN)bizB := NewBizB(dataM)bizC := NewBizC(dataN, dataM)serviceX := NewService(bizA, bizB, bizC)server := NewServer(serviceX)server.httpXXX // 提供 http 服務(wù)server.grpcXXX // 提供 grpc 服務(wù)
}
在 helloworld 這個(gè) demo 當(dāng)中,則是這樣定義 provider 的:
// biz 目錄
var ProviderSet = wire.NewSet(NewGreeterUsecase)type GreeterUsecase struct {repo GreeterRepolog *log.Helper
}func NewGreeterUsecase(repo GreeterRepo, logger log.Logger) *GreeterUsecase {return &GreeterUsecase{repo: repo, log: log.NewHelper(logger)}
}func (uc *GreeterUsecase) CreateGreeter(ctx context.Context, g *Greeter) (*Greeter, error) {uc.log.WithContext(ctx).Infof("CreateGreeter: %v", g.Hello)return uc.repo.Save(ctx, g)
}// data 目錄
var ProviderSet = wire.NewSet(NewData, NewGreeterRepo)type Data struct {// TODO wrapped database client
}func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {cleanup := func() {log.NewHelper(logger).Info("closing the data resources")}return &Data{}, cleanup, nil
}type greeterRepo struct {data *Datalog *log.Helper
}func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {return &greeterRepo{data: data,log: log.NewHelper(logger),}
}
// service 目錄
var ProviderSet = wire.NewSet(NewGreeterService)type GreeterService struct {v1.UnimplementedGreeterServeruc *biz.GreeterUsecase
}func NewGreeterService(uc *biz.GreeterUsecase) *GreeterService {return &GreeterService{uc: uc}
}func (s *GreeterService) SayHello(ctx context.Context, in *v1.HelloRequest) (*v1.HelloReply, error) {g, err := s.uc.CreateGreeter(ctx, &biz.Greeter{Hello: in.Name})if err != nil {return nil, err}return &v1.HelloReply{Message: "Hello " + g.Hello}, nil
}// server 目錄
var ProviderSet = wire.NewSet(NewGRPCServer, NewHTTPServer)func NewGRPCServer(c *conf.Server, greeter *service.GreeterService, logger log.Logger) *grpc.Server {var opts = []grpc.ServerOption{grpc.Middleware(recovery.Recovery(),),}if c.Grpc.Network != "" {opts = append(opts, grpc.Network(c.Grpc.Network))}if c.Grpc.Addr != "" {opts = append(opts, grpc.Address(c.Grpc.Addr))}if c.Grpc.Timeout != nil {opts = append(opts, grpc.Timeout(c.Grpc.Timeout.AsDuration()))}srv := grpc.NewServer(opts...)v1.RegisterGreeterServer(srv, greeter)return srv
}
在 helloworld 這個(gè) demo 當(dāng)中,則是這樣定義 injector 的:
// wire.go
func wireApp(*conf.Server, *conf.Data, log.Logger) (*kratos.App, func(), error) {panic(wire.Build(server.ProviderSet, data.ProviderSet, biz.ProviderSet, service.ProviderSet, newApp))
}
最后運(yùn)行 wire 的到的完成注入的文件如下:
// wire_gen.go
func wireApp(confServer *conf.Server, confData *conf.Data, logger log.Logger) (*kratos.App, func(), error) {dataData, cleanup, err := data.NewData(confData, logger)if err != nil {return nil, nil, err}greeterRepo := data.NewGreeterRepo(dataData, logger)greeterUsecase := biz.NewGreeterUsecase(greeterRepo, logger)greeterService := service.NewGreeterService(greeterUsecase)grpcServer := server.NewGRPCServer(confServer, greeterService, logger)httpServer := server.NewHTTPServer(confServer, greeterService, logger)app := newApp(logger, grpcServer, httpServer)return app, func() {cleanup()}, nil
}
生成代碼之后,則可以像使用普通的 golang 函數(shù)一樣,使用這個(gè) wire_gen.go 文件內(nèi)的 wireApp 函數(shù)實(shí)例化一個(gè) helloworld 服務(wù)
func main() {flag.Parse()logger := log.With(log.NewStdLogger(os.Stdout),// ...)c := config.New(// ...)defer c.Close()// ...app, cleanup, err := wireApp(bc.Server, bc.Data, logger)if err != nil {panic(err)}defer cleanup()// start and wait for stop signalif err := app.Run(); err != nil {panic(err)}
}
4.3 wire 的更多用法
參見 wire 的文檔,自己用幾遍就明白了,這里舉幾個(gè)例子:
- 定義攜帶 error 返回值的 provider
func ProvideBaz(ctx context.Context, bar Bar) (Baz, error) {if bar.X == 0 {return Baz{}, errors.New("cannot provide baz when bar is zero")}return Baz{X: bar.X}, nil
}
- provider 集合:方便組織多個(gè) provider
var SuperSet = wire.NewSet(ProvideFoo, ProvideBar, ProvideBaz)
- 接口綁定:
type Fooer interface {Foo() string
}type MyFooer stringfunc (b *MyFooer) Foo() string {return string(*b)
}func provideMyFooer() *MyFooer {b := new(MyFooer)*b = "Hello, World!"return b
}type Bar stringfunc provideBar(f Fooer) string {// f will be a *MyFooer.return f.Foo()
}var Set = wire.NewSet(provideMyFooer,wire.Bind(new(Fooer), new(*MyFooer)),provideBar)
五、對(duì)比 Spring Boot 的依賴注入
Spring Boot的依賴注入(DI)和Golang開源庫Wire的依賴注入在設(shè)計(jì)思路上存在一些相同點(diǎn)和不同點(diǎn)。以下是對(duì)這些相同點(diǎn)和不同點(diǎn)的分析:
相同點(diǎn)
- 降低耦合度:兩者都通過依賴注入的方式實(shí)現(xiàn)了代碼的松耦合。這意味著,一個(gè)對(duì)象不需要顯式地創(chuàng)建或查找它所依賴的其他對(duì)象,這些依賴項(xiàng)會(huì)由外部容器(如Spring容器)或工具(如Wire)自動(dòng)提供。
- 提高可測(cè)試性:由于依賴關(guān)系被解耦,可以更容易地替換依賴項(xiàng)以進(jìn)行單元測(cè)試。無論是Spring Boot還是使用Wire的Golang應(yīng)用,都可以輕松地為組件提供模擬或存根的依賴項(xiàng)以進(jìn)行測(cè)試。
- 靈活性:兩者都允許在不修改組件代碼的情況下替換依賴項(xiàng)。這使得應(yīng)用程序在維護(hù)和擴(kuò)展時(shí)更加靈活。
不同點(diǎn)
- 實(shí)現(xiàn)方式:
- Spring Boot的依賴注入是基于Java的反射機(jī)制和Spring框架的容器管理功能實(shí)現(xiàn)的。Spring容器負(fù)責(zé)創(chuàng)建和管理Bean的生命周期,并在需要時(shí)自動(dòng)注入依賴項(xiàng),核心在于運(yùn)行時(shí)。
- Wire是一個(gè)Golang的代碼生成工具,它通過分析代碼中的構(gòu)造函數(shù)和結(jié)構(gòu)體標(biāo)簽,自動(dòng)生成依賴注入的代碼(減少人工工作量),在開發(fā)階段已經(jīng)通過工具生成好了依賴注入的代碼,程序編譯時(shí),資源之間的依賴關(guān)系已經(jīng)固定。
- 配置方式:
- Spring Boot的依賴注入通常通過配置文件(如application.properties或application.yml)和注解(如@Autowired)進(jìn)行配置。開發(fā)者可以在配置文件中定義Bean的屬性,并通過注解在需要注入的地方指明依賴關(guān)系。
- Wire則通過特殊的Go文件(通常是wire.go文件)來定義類型之間的依賴關(guān)系。這些文件包含了用于生成依賴注入代碼的指令和元數(shù)據(jù)。
- 運(yùn)行時(shí)開銷:
- Spring Boot的依賴注入在運(yùn)行時(shí)需要依賴Spring容器來管理Bean的生命周期和依賴關(guān)系。這可能會(huì)引入一些額外的運(yùn)行時(shí)開銷,特別是在大型應(yīng)用程序中。
- Wire在編譯時(shí)生成依賴注入的代碼,因此它在運(yùn)行時(shí)沒有額外的開銷。這使得使用Wire的Golang應(yīng)用程序通常具有更好的性能。
六、參考資料
kratos:https://go-kratos.dev/en/docs/getting-started/start/
wire:https://github.com/google/wire/blob/main/_tutorial/README.md