Container - 为 Go 语言而生的运行时依赖注入容器

2020-05-25 17:58:24 +08:00
 mylxsw

写了个 Golang 的依赖注入库,其实去年就已经写了,只是一直懒得写文档,最近刚好有时间,把 API 接口重新梳理了下,然后写了个文档,大家来捧捧场啊!

Container 是一款为 Go 语言开发的运行时依赖注入库。Go 语言的语言特性决定了实现一款类型安全的依赖注入容器并不太容易,因此 Container 大量使用了 Go 的反射机制。如果你的使用场景对性能要求并不是那个苛刻,那 Container 非常适合你。

并不是说对性能要求苛刻的环境中就不能使用了,你可以把 Container 作为一个对象依赖管理工具,在你的业务初始化时获取依赖的对象。

使用方式

go get github.com/mylxsw/container

要创建一个 Container 实例,使用 containier.New 方法

cc := container.New()

此时就创建了一个空的容器。

你也可以使用 container.NewWithContext(ctx) 来创建容器,创建之后,可以自动的把已经存在的 context.Context 对象添加到容器中,由容器托管。

对象绑定

在使用之前,我们需要先将我们要托管的对象告诉容器。Container 支持三种类型的对象管理

所有的对象绑定方法都会返回一个 error 返回值来说明是否绑定成功,应用在使用时一定要主动去检查这个 error

确定对象一定会绑定成功(一般不违反文档中描述的参数签名方式,都是一定会成功的)或者要求对象必须要绑定成功(通常我们都要求这样,不然怎么进行依赖管理呢),则可以使用 Must 系列方法,比如 Singleton 方法对应的时 MustSingleton,当创建出错时,该方法会直接 panic

绑定对象时,SingletonPrototypeBindValue 方法对于同一类型,只能绑定一次,如果多次绑定同一类型对象的创建函数,会返回 ErrRepeatedBind 错误。

有时候,希望对象创建函数可以多次重新绑定,这样就可以个应用更多的扩展性,可以随时替换掉对象的创建方法,比如测试时 Mock 对象的注入。这时候我们可以使用 Override 系列方法:

使用 Override 系列方法时,必须保证第一次绑定时使用的是 Override 系列方法,否则无法重新绑定。

也就是说,可以这样绑定 SingletonOverride -> SingletonOverrideSingletonOverride -> Singleton,但是一旦出现 Singleton,后续就无法对该对象重新绑定了。

单例对象

使用 Singleton 系列的方法来将单例对象托管给容器,单例对象只会在第一次使用时自动完成创建,之后所有对该对象的访问都会自动将已经创建好的对象注入进来。

常用的方法是 Singleton(initialize interface{}) error 方法,该方法会按照你提供的 initialize 函数或者对象来完成单例对象的注册。

参数 initialize 支持以下几种形式:

当对象第一次被使用时,Container 会将对象创建函数的执行结果缓存起来,从而实现任何时候后访问都是获取到的同一个对象。

原型对象(多例对象)

原型对象(多例对象)是指的由 Container 托管对象的创建过程,但是每次使用依赖注入获取到的都是新创建的对象。

使用 Prototype 系列的方法来将原型对象的创建托管给容器。常用的方法是 Prototype(initialize interface{}) error

参数 initialize 可以接受的类型与 Singleton 系列函数完全一致,唯一的区别是在对象使用时,单例对象每次都是返回的同一个对象,而原型对象则是每次都返回新创建的对象。

字符串值对象绑定

这种绑定方式是将某个对象绑定到 Container 中,但是与 Singleton 系列方法不同的是,它要求必须指定一个字符串类型的 Key,每次获取对象的时候,使用 Get 系列函数获取绑定的对象时,直接传递这个字符串 Key 即可。

常用的绑定方法为 BindValue(key string, value interface{})

cc.BindValue("version", "1.0.1")
cc.MustBindValue("startTs", time.Now())
cc.BindValue("int_val", 123)

依赖注入

在使用绑定对象时,通常我们使用 ResolveCall 系列方法。

Resolve

Resolve(callback interface{}) error 方法执行体 callback 内部只能进行依赖注入,不接收注入函数的返回值,虽然有一个 error 返回值,但是该值只表明是否在注入对象时产生错误。

比如,我们需要获取某个用户的信息和其角色信息,使用 Resolve 方法

cc.MustResolve(func(userRepo repo.UserRepo, roleRepo repo.RoleRepo) {
    // 查询 id=123 的用户,查询失败直接 panic
    user, err := userRepo.GetUser(123)
    if err != nil {
        panic(err)
    }
    // 查询用户角色,查询失败时,我们忽略了返回的错误
    role, _ := roleRepo.GetRole(user.RoleID)

    // do something you want with user/role
})

直接使用 Resolve 方法可能并不太满足我们的日常业务需求,因为在执行查询的时候,总是会遇到各种 error,直接丢弃会产生很多隐藏的 Bug,但是我们也不倾向于使用 Panic 这种暴力的方式来解决。

Container 提供了 ResolveWithError(callback interface{}) error 方法,使用该方法时,我们的 callback 可以接受一个 error 返回值,来告诉调用者这里出现问题了。

err := cc.ResolveWithError(func(userRepo repo.UserRepo, roleRepo repo.RoleRepoo) error {
    user, err := userRepo.GetUser(123)
    if err != nil {
        return err
    }

    role, err := roleRepo.GetRole(user.RoleID)
    if err != nil {
        return err
    }

    // do something you want with user/role

    return nil
})
if err != nil {
    // 自定义错误处理
}

Call

Call(callback interface{}) ([]interface{}, error) 方法不仅完成对象的依赖注入,还会返回 callback 的返回值,返回值为数组结构。

比如

results, err := cc.Call(func(userRepo repo.UserRepo) ([]repo.User, error) {
    users, err := userRepo.AllUsers()
    return users, err
})
if err != nil {
    // 这里的 err 是依赖注入过程中的错误,比如依赖对象创建失败
}

// results 是一个类型为 []interface{} 的数组,数组中按次序包含了 callback 函数的返回值
// results[0] - []repo.User
// results[1] - error
// 由于每个返回值都是 interface{} 类型,因此在使用时需要执行类型断言,将其转换为具体的类型再使用
users := results[0].([]repo.User)
err := results[0].(error)

Provider

有时我们希望为不同的功能模块绑定不同的对象实现,比如在 Web 服务器中,每个请求的 handler 函数需要访问与本次请求有关的 request/response 对象,请求结束之后,Container 中的 request/response 对象也就没有用了,不同的请求获取到的也不是同一个对象。我们可以使用 CallWithProvider(callback interface{}, provider func() []*Entity) ([]interface{}, error) 配合 Provider(initializes ...interface{}) (func() []*Entity, error) 方法实现该功能。

ctxFunc := func() Context { return ctx }
requestFunc := func() Request { return ctx.request }

provider, _ := cc.Provider(ctxFunc, requestFunc)
results, err := cc.CallWithProvider(func(userRepo repo.UserRepo, req Request) ([]repo.User, error) {
    // 这里我们注入的 Request 对象,只对当前 callback 有效
    userId := req.Input("user_id")
    users, err := userRepo.GetUser(userId)
    
    return users, err
}, provider)

AutoWire 结构体属性注入

使用 AutoWire 方法可以为结构体的属性注入其绑定的对象,要使用该特性,我们需要在需要依赖注入的结构体对象上添加 autowire 标签。

type UserManager struct {
    UserRepo *UserRepo `autowire:"@" json:"-"`
    field1   string    `autowire:"version"`
    Field2   string    `json:"field2"`
}

manager := UserManager{}
// 对 manager 执行 AutoWire 之后,会自动注入 UserRepo 和 field1 的值
if err := c.AutoWire(&manager); err != nil {
    t.Error("test failed")
}

结构体属性注入支持公开和私有字段的注入。如果对象是通过类型来注入的,使用 autowire:"@" 来标记属性;如果使用的是 BindValue 绑定的字符串为 key 的对象,则使用 autowire:"Key 名称" 来标记属性。

由于 AutoWire 要修改对象,因此必须使用对象的指针,结构体类型必须使用 &

其它方法

HasBound/HasBoundValue

方法签名

HasBound(key interface{}) bool
HasBoundValue(key string) bool

用于判断指定的 Key 是否已经绑定过了。

Keys

方法签名

Keys() []interface{}

获取所有绑定到 Container 中的对象信息。

CanOverride

方法签名

CanOverride(key interface{}) (bool, error)

判断指定的 Key 是否可以覆盖,重新绑定创建函数。

Extend

Extend 并不是 Container 实例上的一个方法,而是一个独立的函数,用于从已有的 Container 生成一个新的 Container,新的 Container 继承已有 Container 所有的对象绑定。

Extend(c Container) Container

容器继承之后,在依赖注入对象查找时,会优先从当前 Container 中查找,当找不到对象时,再从父对象查找。

在 Container 实例上个,有一个名为 ExtendFrom(parent Container) 的方法,该方法用于指定当前 Container 从 parent 继承。

示例项目

简单的示例可以参考项目的 example 目录。

以下项目中使用了 Container 作为依赖注入管理库,感兴趣的可以参考一下。

1506 次点击
所在节点    程序员
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/675338

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX