# #go #dependency-injection
Вопрос:
Я использую github.com/google/wire
для внедрения зависимостей в примере проекта с открытым исходным кодом, над которым я работаю.
У меня есть следующие интерфейсы в пакете с именем interfaces
:
type LoginService interface {
Login(email, password) (*LoginResult, error)
}
type JWTService interface {
Generate(user *models.User) (*JWTGenerateResult, error)
Validate(tokenString string) (*JWTValidateResult, error)
}
type UserDao interface {
ByEmail(email string) (*models.User, error)
}
У меня есть реализации, которые выглядят так:
type LoginServiceImpl struct {
jwt interfaces.JWTService
dao interfaces.UserDao
logger *zap.Logger
}
func NewLoginService(jwt interfaces.JWTService, dao interfaces.UserDao,
logger *zap.Logger) *LoginServiceImpl {
return amp;LoginServiceImpl{jwt: jwt, dao: dao, logger: logger }
}
type JWTServiceImpl struct {
key [32]byte
logger *zap.Logger
}
func NewJWTService(key [32]byte, logger *zap.Logger) (*JWTServiceImpl, error) {
r := JWTServiceImpl {
key: key,
logger: logger,
}
if !r.safe() {
return nil, fmt.Errorf("unable to create JWT service, unsafe key: %s", err)
}
return amp;r, nil
}
type UserDaoImpl struct {
db: *gorm.DB
logger: *zap.Logger
}
func NewUserDao(db *gorm.DB, logger *zap.Logger) *UserDao {
return amp;UserDaoImpl{ db: db, logger: logger }
}
Я исключу здесь другие заводские функции и реализации, потому что все они очень похожи. Они могут возвращать ошибку или быть непогрешимыми.
У меня есть еще одна интересная фабрика для создания подключения к базе данных, в которой я просто покажу интерфейс, а не реализацию:
func Connect(config interfaces.MySQLConfig) (*gorm.DB, error) { /* ... */ }
Теперь перейдем к проблеме. В моей точке входа командной строки я создаю регистратор:
logger, err := zap.NewDevelopment()
Для каждого из приведенных выше заводских методов мне нужно предоставить регистратор, а не один и тот же экземпляр регистратора, как если бы эти методы вызывались следующим образом:
logger, err := zap.NewDevelopment()
// check err
db, err := database.Connect(config)
// check err
userDao := dao.NewUserDao(db, logger.Named("dao.user"))
jwtService, err := service.NewJWTService(jwtKey)
// check err
loginService := service.NewLoginService(jwtService, userDao, logger.Named("service.login"))
Моя wire.ProviderSet
конструкция выглядит так:
wire.NewSet(
wire.Bind(new(interfaces.LoginService), new(*service.LoginServiceImpl)),
wire.Bind(new(interfaces.JWTService), new(*service.JWTServiceImpl)),
wire.Bind(new(interfaces.UserDao), new(*dao.UserDaoImpl)),
service.NewLoginService,
service.NewJWTService,
dao.NewUserDao,
database.Connect,
)
Я прочитал руководство пользователя, учебник и рекомендации, и, похоже, не могу найти способ направить уникальный zap.Logger
для каждого из этих заводских методов и направить случайный [32]byte
для службы JWT.
Поскольку мой корневой регистратор не создается во время компиляции, и поскольку каждому из этих заводских методов требуется свой собственный уникальный регистратор, как мне указать wire
, чтобы привязать эти экземпляры к соответствующим заводским методам? Мне трудно понять, как перенаправлять пользовательские экземпляры одного и того же типа в разные заводские методы.
Вкратце:
Wire, похоже, предпочитает все делать во время компиляции, сохраняя конфигурацию внедрения зависимостей в статической переменной уровня пакета. Для большинства моих вариантов использования это нормально.
В остальной части моего варианта использования мне нужно создать несколько экземпляров вручную, прежде чем запускать инъекцию зависимостей и возможность направлять различные
*zap.Logger
экземпляры в каждую службу, которой это нужно.По сути, мне нужно что-то
wire
сделатьservices.NewUserDao(Connect(mysqlConfig), logger.Named("dao.user")
, но я не знаю, как выразить этоwire
и объединить переменные во время выполнения сwire
помощью подхода времени компиляции.
Как мне это сделать wire
?
Комментарии:
1. можете ли вы попытаться сузить его до одного исполняемого файла ? даже несмотря на то, что у вас есть несколько вопросов.
2. Короче говоря: «Мне нужно создать
wire
набор и направить живые экземпляры во внедрение зависимостей в мои заводские методы, а не просто фабрики и структуры во время компиляции».3. @mh-cbon Я обновил свой вопрос кратким резюме в конце, в котором подробно описана проблема и то, что мне нужно уметь делать.
Ответ №1:
Мне пришлось несколько изменить то, что я делал, как рекомендуется в документации:
Если вам нужно ввести общий тип , например
string
, создайте новый строковый тип, чтобы избежать конфликтов с другими поставщиками. Например:type MySQLConnectionString string
Добавление Пользовательских Типов
Документация, по общему признанию, очень скупа, но в итоге я создал кучу типов:
type JWTKey [32]byte
type JWTServiceLogger *zap.Logger
type LoginServiceLogger *zap.Logger
type UserDaoLogger *zap.Logger
Обновление Функций Производителя
Я обновил свои методы производителя, чтобы принять эти типы, но мне не пришлось обновлять свои структуры:
// LoginServiceImpl implements interfaces.LoginService
var _ interfaces.LoginService = (*LoginServiceImpl)(nil)
type LoginServiceImpl struct {
dao interfaces.UserDao
jwt interfaces.JWTService
logger *zap.Logger
}
func NewLoginService(dao interfaces.UserDao, jwt interfaces.JWTService,
logger LoginServiceLogger) *LoginServiceImpl {
return amp;LoginServiceImpl {
dao: dao,
jwt: jwt,
logger: logger,
}
}
Эта вышеприведенная часть имела смысл; предоставление различных типов означало, что wire
нужно было меньше выяснять.
Создание инжектора
Затем я должен был создать фиктивный инжектор, а затем использовать wire
его для создания соответствующего wire_gen.go
. Это было нелегко и очень неинтуитивно. При следовании документации что-то постоянно ломалось и выдавало мне очень бесполезные сообщения об ошибках.
У меня есть cmd/
пакет , в котором находится моя точка входа в CLI cmd/serve/root.go
, которая запускается ./api serve
из командной строки. Я создал свою функцию инжектора в cmd/serve/injectors.go
, обратите внимание, что // build wireinject
и следующая новая строка необходимы, чтобы сообщить Go, что этот файл используется для генерации кода, а не для самого кода.
В конечном итоге я пришел к следующему коду после долгих проб и ошибок:
// build wireinject
package serve
import /*...*/
func initializeLoginService(
config interfaces.MySQLConfig,
jwtKey service.JWTKey,
loginServiceLogger service.LoginServiceLogger,
jwtServiceLogger service.JWTServiceLogger,
userDaoLogger service.UserDaoLogger,
databaseLogger database.DatabaseLogger,
) (interfaces.LoginService, error) {
wire.Build(
// bind interfaces to implementations
wire.Bind(new(interfaces.LoginService), new(*service.LoginServiceImpl)),
wire.Bind(new(interfaces.JWTService), new(*service.JWTServiceImpl)),
wire.Bind(new(interfaces.UserDao), new(*dao.UserDao)),
// services
service.NewLoginService,
service.NewJWTService,
// daos
dao.NewUserDao,
// database
database.Connect,
)
return nil, nil
}
wire.Bind
Вызовы сообщают wire
, какую реализацию использовать для данного интерфейса, чтобы он знал, что service.NewLoginService
то, что возвращает a *LoginServiceImpl
, должно использоваться в качестве interfaces.LoginService
.
Остальные сущности в вызове wire.Build
— это просто заводские функции.
Передача значений в инжектор
Одна из проблем, с которой я столкнулся, заключалась в том, что я пытался передать значения, как wire.Build
описано в документации:
Иногда бывает полезно привязать базовое значение (обычно ноль) к типу. Вместо того, чтобы инжекторы зависели от функции одноразового поставщика, вы можете добавить выражение значения в набор поставщиков.
type Foo struct { X int } func injectFoo() Foo { wire.Build(wire.Value(Foo{X: 42})) return Foo{} }
…
Важно отметить, что выражение будет скопировано в пакет инжектора; ссылки на переменные будут вычисляться во время инициализации пакета инжектора. Провод выдаст ошибку, если выражение вызывает какие-либо функции или получает данные из каких-либо каналов.
Это то, что меня смутило; казалось, что вы действительно можете использовать постоянные значения только при попытке запустить инжектор, но в документах в разделе «инжекторы» есть две строки:
Как и поставщики, инжекторы могут быть параметризованы на входах (которые затем отправляются поставщикам) и могут возвращать ошибки. Аргументы для телеграфирования.Сборка такая же, как и провод.Набор новостей: они образуют набор поставщиков. Это набор поставщиков, который используется во время генерации кода для этого инжектора.
Эти строки сопровождаются этим кодом:
func initializeBaz(ctx context.Context) (foobarbaz.Baz, error) {
wire.Build(foobarbaz.MegaSet)
return foobarbaz.Baz{}, nil
}
Это то, что я пропустил и что заставило меня потерять на этом много времени. context.Context
похоже, в этом коде нигде не передается, и это распространенный тип, поэтому я просто отмахнулся от него и не извлек из него уроков.
Я определил свою функцию инжектора, чтобы принимать аргументы для ключа JWT, конфигурации MySQL и типов регистраторов:
func initializeLoginService(
config interfaces.MySQLConfig,
jwtKey service.JWTKey,
loginServiceLogger service.LoginServiceLogger,
jwtServiceLogger service.JWTServiceLogger,
userDaoLogger service.UserDaoLogger,
databaseLogger database.DatabaseLogger,
) (interfaces.LoginService, error) {
// ...
return nil, nil
}
Затем я попытался ввести их в wire.Build
:
wire.Build(
// ...
wire.Value(config),
wire.Value(jwtKey),
wire.Value(loginServiceLogger),
// ...
)
Когда я попытался запустить wire
, он пожаловался, что эти типы были определены дважды. Я был очень смущен таким поведением, но в конечном итоге узнал, что wire
автоматически отправляет все параметры функции в wire.Build
.
Еще раз: wire
автоматически отправляет все параметры функции инжектора в wire.Build
.
Это не было интуитивно понятным для меня, но я на собственном горьком опыте убедился, что так оно wire
и работает.
Краткие сведения
wire
не предоставляет ему возможности различать значения одного и того же типа в своей системе внедрения зависимостей. Таким образом, вам нужно обернуть эти простые типы определениями типов, чтобы wire
знать, как их направлять, поэтому вместо [32]byte
, type JWTKey [32]byte
.
Чтобы ввести живые значения в ваш wire.Build
вызов, просто измените сигнатуру функции инжектора, чтобы включить эти значения в параметры функции и wire
автоматически ввести их wire.Build
.
Запустите cd pkg/my/package amp;amp; wire
, чтобы создать wire_gen.go
в этом каталоге определенные вами форсунки. Как только это будет сделано, будущие вызовы go generate
будут автоматически обновляться wire_gen.go
по мере внесения изменений.
У меня есть wire_gen.go
файлы, зарегистрированные в моей системе управления версиями (VCS), которая является Git, что кажется странным из-за того, что они генерируются артефактами сборки, но, похоже, это обычно делается именно так. Возможно , было бы более выгодно исключить wire_gen.go
, но если вы сделаете это, вам нужно будет найти каждый пакет, содержащий файл с // build wireinject
заголовком, запустить wire
в этом каталоге, а затем go generate
просто для уверенности.
Надеюсь, это прояснит способ wire
работы с фактическими значениями: сделайте их безопасными для ввода с помощью оберток типов, просто передайте их в функцию инжектора и wire
сделайте все остальное.