DI (Dependency Injection)
依赖注入,即将组件的依赖关系外部化,而不是由组件内部创建和管理这些依赖。这样可以提高系统的可测试性、可维护性和扩展性。
例如,考虑以下 Go 代码,为 UserCache
组件创建一个 Redis 客户端
type UserCache struct {
client *redis.Client
}
func NewUserCache() *UserCache {
return &UserCache{redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})}
}
func (cache *UserCache) Get(ctx context.Context, id uint64) (domain.User, error) {
......
}
这种写法有几个弊病,不管什么东西都要自己内部创建:
- 内部创建依赖、耦合性高:
NewUserCache
这个方法内部直接创建了redis.Client
,意味着它要知道如何初始化redis.Client
,在这部分逻辑扩展后,若客户端更换,这段代码更改起来比较麻烦。
- 测试困难:
- 在进行单元测试时,直接依赖外部服务会带来很多问题。外部服务可能会不稳定、响应慢,甚至在测试环境中不可用,这会导致测试结果不稳定。而在模拟(mock)
redis.Client
时,如果它是在UserCache
内部创建的,就很难替换成一个假的实现,进而难以进行有效的单元测试。
- 在进行单元测试时,直接依赖外部服务会带来很多问题。外部服务可能会不稳定、响应慢,甚至在测试环境中不可用,这会导致测试结果不稳定。而在模拟(mock)
为了解决上述问题,依赖注入(DI)提供了一种更好的设计方式。在依赖注入的模式下,我们可以将 redis.Client
的创建交由外部管理,而 UserCache
只需要接受一个 redis.Client
实例作为参数:
func NewUserCache(client *redis.Client) *UserCache {
return &UserCache{
client: client,
}
}
尽管它已经符合依赖注入,但这种写法仍然存在一些问题。虽然它解决了内部创建的问题,但它仍然依赖于一个具体的 redis 客户端,如果更换客户端是需要更改参数的。对于它,有一种更符合依赖注入的写法
func NewUserCache(client redis.Cmdable) *UserCache {
return &UserCache{
client: client,
}
}
redis.Cmable
是 Go 中的一个接口,代表 redis 客户端的行为,是一种抽象,它不依赖于具体的 redis 客户端,因此如果将来更换客户端,只要它实现了该接口,这个构造方法的代码就无需更改,并且在 mock 时,mock工具也可以根据这个接口所定义的行为去模拟一个 redis 客户端。