Платформа внедрения зависимостей Golang Wire Raiders

Go

В предыдущей серии статей о модульном тестировании я кратко представилинфраструктура внедрения зависимостей проводов. Но Wire в то время все еще находился в стадии альфа-тестирования, но недавно Wire выпустил первую бета-версию, API претерпел некоторые изменения, и он также обещает, что если в этом нет крайней необходимости, он не нарушит совместимость API. В предыдущей статье были представлены некоторые основные обзоры проволоки, которые не будут повторяться в этой статье.Заинтересованные друзья могут оглянуться назад:Get Go Unit Testing (4) — Платформа внедрения зависимостей (проводная версия). В этой статье будет подробно рассказано об использовании проводов и некоторых передовых методах.

Полный пример кода в этом посте можно найти здесь:wire-examples

Installing

go get github.com/google/wire/cmd/wire

Quick Start

Давайте сначала пройдем простой пример, пусть друзья знаютwireЕсть интуитивное понимание. В следующем примере показан простойwireПример внедрения зависимостей:

$ ls
main.go  wire.go 

main.go

package main

import "fmt"

type Message struct {
	msg string
}
type Greeter struct {
	Message Message
}
type Event struct {
	Greeter Greeter
}
// NewMessage Message的构造函数
func NewMessage(msg string) Message {
	return Message{
		msg:msg,
	}
}
// NewGreeter Greeter构造函数
func NewGreeter(m Message) Greeter {
	return Greeter{Message: m}
}
// NewEvent Event构造函数
func NewEvent(g Greeter) Event {
	return Event{Greeter: g}
}
func (e Event) Start() {
	msg := e.Greeter.Greet()
	fmt.Println(msg)
}
func (g Greeter) Greet() Message {
	return g.Message
}

// 使用wire前
func main() {
	message := NewMessage("hello world")
	greeter := NewGreeter(message)
	event := NewEvent(greeter)

	event.Start()
}
/*
// 使用wire后
func main() {
	event := InitializeEvent("hello_world")

	event.Start()
}*/

wire.go

// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import "github.com/google/wire"

// InitializeEvent 声明injector的函数签名
func InitializeEvent(msg string) Event{
	wire.Build(NewEvent, NewGreeter, NewMessage)
	return Event{}  //返回值没有实际意义,只需符合函数签名即可
}

перечислитьwireКоманда для создания файлов зависимостей:

$ wire
wire: github.com/DrmagicE/wire-examples/quickstart: wrote XXXX\github.com\DrmagicE\wire-examples\quickstart\wire_gen.go
$ ls
main.go  wire.go  wire_gen.go

Файл, сгенерированный проводом wire_gen.go

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func InitializeEvent(msg string) Event {
	message := NewMessage(msg)
	greeter := NewGreeter(message)
	event := NewEvent(greeter)
	return event
}

Перед использованием VS После использования

...
/*
// 使用wire前
func main() {
	message := NewMessage("hello world")
	greeter := NewGreeter(message)
	event := NewEvent(greeter)

	event.Start()
}*/

// 使用wire后
func main() {
	event := InitializeEvent("hello_world")

	event.Start()
}
...

использоватьwireПосле этого просто вызовите метод инициализации, чтобы получитьEventТеперь, по сравнению с тем, что было до использования, не только на три строчки кода меньше, но и нет необходимости заботиться о порядке инициализации между зависимостями.

Пример портала:quickstart

Provider & Injector

providerиinjectorдаwireдва основных понятия.

provider: a function that can produce a value. These functions are ordinary Go code.
injector: a function that calls providers in dependency order. With Wire, you write the injector's signature, then Wire generates the function's body.
GitHub.com/Google/wire…

предоставляяproviderфункция, пустьwireЗнайте, как генерировать эти зависимые объекты.wireПо нашему определениюinjectorсигнатура функции, генерирующая полнуюinjectorфункция,injectorФункция — это та, которая нам нужна в конце, и она будет вызываться в порядке зависимостей.provider.

На примере QuickStartNewMessage,NewGreeter,NewEventобаprovider,wire_gen.goсерединаInitializeEventфункцияinjector, можно увидетьinjectorпутем вызова в порядке зависимостиproviderдля создания нужных нам объектовEvent.

Приведенный выше пример находится вwire.goопределено вinjectorПодпись функции, обратите внимание на добавление первой строки файла

// +build wireinject
...

Используется, чтобы сообщить компилятору, что файл не нужно компилировать. существуетinjectorв функции определения подписи, вызвавwire.Buildметод, указывающий метод, используемый для создания зависимостейprovider:

// InitializeEvent 声明injector的函数签名
func InitializeEvent(msg string) Event{
	wire.Build(NewEvent, NewGreeter, NewMessage) // <--- 传入provider函数
	return Event{}  //返回值没有实际意义,只需符合函数签名即可
}

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

Расширенные возможности

Пример быстрого запуска показываетwireОсновные функции , в этом разделе представлены некоторые дополнительные возможности.

привязка интерфейса

В соответствии с принципом инверсии зависимостей объекты должны зависеть от интерфейса, а не напрямую от конкретной реализации.

Абстрагирование зависимостей интерфейса более полезно для модульного тестирования!
Приступайте к модульному тестированию (1) — основные принципы
Get Go Unit Testing (2) — Mock Framework (gomock)

Все зависимости в примере с кратким руководством являются конкретными реализациями. Теперь давайте взглянем наwireКак обрабатывать зависимости интерфейса в:

// UserService 
type UserService struct {
	userRepo UserRepository // <-- UserService依赖UserRepository接口
}

// UserRepository 存放User对象的数据仓库接口,比如可以是mysql,restful api ....
type UserRepository interface {
	// GetUserByID 根据ID获取User, 如果找不到User返回对应错误信息
	GetUserByID(id int) (*User, error)
}
// NewUserService *UserService构造函数
func NewUserService(userRepo UserRepository) *UserService {
	return &UserService{
		userRepo:userRepo,
	}
}

// mockUserRepo 模拟一个UserRepository实现
type mockUserRepo struct {
	foo string
	bar int
}
// GetUserByID UserRepository接口实现
func (u *mockUserRepo) GetUserByID(id int) (*User,error){
	return &User{}, nil
}
// NewMockUserRepo *mockUserRepo构造函数
func NewMockUserRepo(foo string,bar int) *mockUserRepo {
	return &mockUserRepo{
		foo:foo,
		bar:bar,
	}
}
// MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))

В этом примереUserServiceполагатьсяUserRepositoryинтерфейс, которыйmockUserRepoдаUserRepositoryРеализация , поскольку в лучших практиках Go рекомендуется возвращать конкретную реализацию, а не интерфейс. такmockUserRepoизproviderФункция возвращает*mockUserRepoэтот конкретный тип.wireНевозможно автоматически связать конкретные реализации с интерфейсами, нам нужно явно объявить связь между ними. пройти черезwire.NewSetиwire.Bindбудет*mockUserRepoиUserRepositoryСвязывать:

// MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))

определениеinjectorподпись функции:

...
func InitializeUserService(foo string, bar int) *UserService{
	wire.Build(NewUserService,MockUserRepoSet) // 使用MockUserRepoSet
	return nil
}
...

Пример портала:binding-interfaces

вернуть ошибку

В предыдущем примере нашproviderФункции имеют только одно возвращаемое значение, но в некоторых случаяхproviderФункция может проверять входные параметры, если параметр неверен, она должна вернутьerror.wireЭта ситуация также рассматривалась,providerФункция может установить второй параметр возвращаемого значения равнымerror:

// Config 配置
type Config struct {
    // RemoteAddr 连接的远程地址
	RemoteAddr string
	
}
// APIClient API客户端
type APIClient struct {
	c Config
}
// NewAPIClient  APIClient构造函数,如果入参校验失败,返回错误原因
func NewAPIClient(c Config) (*APIClient,error) { // <-- 第二个参数设置成error
	if c.RemoteAddr == "" {
		return nil, errors.New("没有设置远程地址")
	}
	return &APIClient{
		c:c,
	},nil
}
// Service
type Service struct {
	client *APIClient
}
// NewService Service构造函数
func NewService(client *APIClient) *Service{
	return &Service{
		client:client,
	}
}

сродни,injectorКогда функция определена, вам также необходимо установить второе возвращаемое значение равнымerror:

...
func InitializeClient(config Config) (*Service, error) { // <-- 第二个参数设置成error
	wire.Build(NewService,NewAPIClient)
	return nil,nil
}
...

наблюдатьwireСгенерированоinjector:

func InitializeClient(config Config) (*Service, error) {
	apiClient, err := NewAPIClient(config)
	if err != nil { // <-- 在构造依赖的顺序中如果发生错误,则会返回对应的"零值"和相应错误
		return nil, err
	}
	service := NewService(apiClient)
	return service, nil
}

Если ошибка возникает в порядке построения зависимостей, возвращается соответствующее «нулевое значение» и соответствующая ошибка.

Пример портала:return-error

Cleanup functions

когдаproviderСгенерированные объекты нуждаются в некоторой обработке очистки, такой как закрытие файлов, закрытие соединений с базой данных и т. д., вы все равно можете установитьproviderВозвращаемое значение для достижения этого эффекта:

// FileReader
type FileReader struct {
	f *os.File
}
// NewFileReader *FileReader 构造函数,第二个参数是cleanup function
func NewFileReader(filePath string) (*FileReader, func(), error){
	f, err := os.Open(filePath)
	if err != nil {
	    return nil,nil,err
	}
	fr := &FileReader{
	    f:f,
	}
	fn := func() {
	    log.Println("cleanup") 
	    fr.f.Close()
	}
	return fr,fn,nil
}

Подобно возврату ошибки,providerВторой возвращаемый параметр установлен вfunc()Используется для возврата функции очистки, которая возвращается в третьем параметре в приведенном выше примере.error, но это необязательно:

Wire указывает количество и порядок возвращаемых значений провайдера:

  1. Первый параметр — это объект зависимости, который необходимо сгенерировать.
  2. Если возвращаются 2 возвращаемых значения, второй параметр должен быть func() или error
  3. Если возвращаются 3 возвращаемых значения, второй параметр должен быть func(), а третий параметр должен быть ошибкой.

Пример портала:cleanup-functions

Provider set

когда некоторыеproviderОбычно при совместном использовании вы можете использоватьprovider setЧтобы организовать их, используйте пример быстрого запуска в качестве шаблона с небольшими изменениями:

// NewMessage Message的构造函数
func NewMessage(msg string) Message {
	return Message{
		msg:msg,
	}
}
// NewGreeter Greeter构造函数
func NewGreeter(m Message) Greeter {
	return Greeter{Message: m}
}
// NewEvent Event构造函数
func NewEvent(g Greeter) Event {
	return Event{Greeter: g}
}
func (e Event) Start() {
	msg := e.Greeter.Greet()
	fmt.Println(msg)
}
// EventSet Event通常是一起使用的一个集合,使用wire.NewSet进行组合
var EventSet  = wire.NewSet(NewEvent, NewMessage, NewGreeter) // <--

В приведенном выше примереEventи его зависимости черезwire.NewSetкомбинированный, в целом вinjectorВ определении сигнатуры функции используются:

func InitializeEvent(msg string) Event{
	//wire.Build(NewEvent, NewGreeter, NewMessage)
	wire.Build(EventSet) 
	return Event{}
}

Тогда просто поставьEventSetвходящийwire.BuildВот и все.

Пример портала:provider-set

поставщик структуры

В дополнение к функциям структуры также могут выполнять функции.providerроль, похожая наsetterвпрыск:

type Foo int
type Bar int

func ProvideFoo() Foo {
	return 1
}
func ProvideBar() Bar {
	return 2
}
type FooBar struct {
	MyFoo Foo
	MyBar Bar
}
var Set = wire.NewSet(
	ProvideFoo,
	ProvideBar,
	wire.Struct(new(FooBar), "MyFoo", "MyBar"))

пройти черезwire.StructЧтобы указать, какие поля должны быть введены в структуру, если это все поля, это также можно сократить как:

var Set = wire.NewSet(
	ProvideFoo,
	ProvideBar,
	wire.Struct(new(FooBar), "*")) // * 表示注入全部字段

Сгенерированоinjectorфункция:

func InitializeFooBar() FooBar {
	foo := ProvideFoo()
	bar := ProvideBar()
	fooBar := FooBar{
		MyFoo: foo,
		MyBar: bar,
	}
	return fooBar
}

Пример портала:struct-provider

Best Practices

Отличить тип

так какinjectorВ функции не допускаются повторяющиеся типы параметров, иначеwireНе будет никакого способа различить эти же типы параметров, такие как:

type FooBar struct {
	foo string
	bar string
}

func NewFooBar(foo string, bar string) FooBar {
	return FooBar{
	    foo: foo,  
	    bar: bar,
	}
}

injectorОпределение сигнатуры функции:

// wire无法得知入参a,b跟FooBar.foo,FooBar.bar的对应关系
func InitializeFooBar(a string, b string) FooBar {
	wire.Build(NewFooBar)
	return FooBar{}
}

Если вы используете вышеперечисленноеproviderгенерироватьinjector,wireБудет сообщена следующая ошибка:

provider has multiple parameters of type string

Поскольку все входные параметры являются строковыми, wire не может знать соответствие между входными параметрами a, b и FooBar.foo, FooBar.bar. Поэтому мы используем разные типы, чтобы избежать конфликтов:

type Foo string
type Bar string
type FooBar struct {
	foo Foo
	bar Bar
}

func NewFooBar(foo Foo, bar Bar) FooBar {
	return FooBar{
	    foo: foo,
	    bar: bar,
	}
}

injectorОпределение сигнатуры функции:

func InitializeFooBar(a Foo, b Bar) FooBar {
	wire.Build(NewFooBar)
	return FooBar{}
}

Среди них базовые типы и общие типы интерфейсов чаще всего конфликтуют, если они находятся вproviderпоявляется в функции, лучше всего создать новый псевдоним для его замены (хотя конфликта нет), например:

type MySQLConnectionString string
type FileReader io.Reader

пример порталаdistinguishing-types

Options Structs

еслиproviderМетод содержит множество зависимостей, которые можно поместить в структуру параметров, чтобы избежать слишком большого количества параметров для конструктора:

type Message string

// Options
type Options struct {
	Messages []Message
	Writer   io.Writer
	Reader   io.Reader
}
type Greeter struct {
}

// NewGreeter Greeter的provider方法使用Options以避免构造函数过长
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
	return nil, nil
}
// GreeterSet 使用wire.Struct设置Options为provider
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)

Сигнатура функции инжектора:

func InitializeGreeter(ctx context.Context, msg []Message, w io.Writer, r io.Reader) (*Greeter, error) {
	wire.Build(GreeterSet)
	return nil, nil
}

пример порталаoptions-structs

Некоторые недостатки и ограничения

дополнительные определения типов

так какwireсобственные ограничения,injectorТипы переменных не могут быть продублированы, что требует определения многих дополнительных псевдонимов базовых типов.

фиктивная поддержка временно недостаточно дружелюбна

В настоящее времяwireкоманда еще не распознана_test.goв конечном файлеproviderфункция, а это значит, что при необходимости она также используется в тестеwireЧтобы внедрить наш фиктивный объект, нам нужно внедрить фиктивный объектprovider, который навязчив к обычному коду, но чиновник, кажется, заметил эту проблему.Заинтересованные партнеры могут обратить внимание на этот вопрос:GitHub.com/Google/wire…

больше ссылок

Официальный README.md
Официальный гид.md
Официальные лучшие практики.md