Создание регистраторов для каждого Поставщика в инъекции зависимости от проводов

# #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 сделайте все остальное.