什么是依赖注入
在了解依赖注入之前,我们可以先分析下什么是依赖。
依赖
依赖的日常解释是依靠别人或事物而不能自利或自给,在软件开发中,依赖则表示的是函数,对象,模块之间的引用关系,比如函数调用,对象引用。
类似日常解释中,缺失依赖,人或事物不能自给或自利,同样,软件系统中缺失了依赖,也无法正常运行或发生正常的运行行为。
与日常依赖不同的是,软件系统的依赖,倡导的是单向依赖关系,也即A引用B,则不建议B再引用A,go语言中则直接通过不支持循环引用这一特点,迫使达到这一目标。一般有如下依赖关系:
方法依赖
go 体验AI代码助手 代码解读复制代码 func A() {
...
}
//B方法的依赖于A方法,才能完整运行
func B() {
...
A()
}
对象依赖
go 体验AI代码助手 代码解读复制代码type A struct{
}
// B结构依赖A结构,组成了一个完整体
type B struct {
a A
}
依赖关系不限于此,由此可以看出系统通过依赖运行,但因为依赖的存在,增强了模块或系统之间耦合。
降低耦合度是系统设计或代码设计中永恒的主题。
依赖注入
注入的意思,可以理解为从外部输入,而非内部产生。软件系统中,可以理解为依赖项不是由使用方(方法/对象)生成,而是从外部传入的一个过程。
依赖注入(Dependency injection, 缩写DI) 是一种软件设计模式,设计的目的是为了分离关注点,分离接收方和依赖,降低耦合度以及提升代码的重用性。优点如下:
有助于代码测试
提高代码的可复用性
降低耦合度,有助于代码的维护
依赖注入不是编码的目标,它是为了达到松耦合的一种手段。
go实现的步骤
步骤
抽象,定义接口/类型(对于方法而言)
建立接口/方法具体实现
设计传入依赖项的方法
使用注入的依赖
方法依赖注入
假设我们要实现一个消息通知服务,可能会支持不同的发送方式,我们仅通过方法进行。
常规写法
go 体验AI代码助手 代码解读复制代码
func main() {
sendMsg("DI Test!", "sms")
}
// 使用方,根据需要根据channel值选择依赖方法,后续有新的方法需要改动,且不好测试。
func sendMsg(msg string, channel string) error {
if channel == "email" {
return sendByEmail(msg)
}
if channel == "sms" {
return sendByEmail(msg)
}
return errors.New("Invalid Channel")
}
func sendByEmail(msg string) error {
fmt.Printf("send msg by Email, msg:%s\n", msg)
return nil
}
func sendBySms(msg string) error {
fmt.Printf("send msg by SMS, msg:%s\n", msg)
return nil
}
上述代码中,假设我们实现一种新的发送方式, 除了需要新增一个sendByXX方法之外,还需要修改sendMsg方法,以适配新的sendByXX方法,sendMsg与发送方式耦合过紧,得益于go中函数一等公民特性,我们可以通过定义函数类型,进行改造, 如下。
依赖注入写法
go 体验AI代码助手 代码解读复制代码// 定义方法类型
type SendFunc func(msg string) error
func main() {
sendMsg("DI Test!", sendBySms)
}
// 允许注入依赖方法
func sendMsg(msg string, sf SendFunc) error {
return sf(msg)
}
func sendByEmail(msg string) error {
fmt.Printf("send msg by Email, msg:%s\n", msg)
return nil
}
func sendBySms(msg string) error {
fmt.Printf("send msg by SMS, msg:%s\n", msg)
return nil
}
改造后,抽象了SendFunc的类型, 新增一个sendByXX方法时,只需在调用处传入此方法即可。sendMsg中无需关注sendByXX方法的存在。测试时,只需要实现一个sendByMock方法,传入即可。
接口依赖注入
同样还是实现消息通知,采取面向对象编程方式;
常规写法
go 体验AI代码助手 代码解读复制代码type Notify struct {
channel string
}
func (n *Notify) Send(msg string) error {
channel := n.channel
// 依赖channel值
if channel == "email" {
e := new(Email)
return e.Send(msg)
}
if channel == "sms" {
sms := new(SMS)
return sms.Send(msg)
}
return errors.New("Invalid Channel:" + channel)
}
func NewNotify(channel string) *Notify {
return &Notify{
channel,
}
}
type Email struct {
}
func (e *Email) Send(msg string) error {
fmt.Printf("Email Send, msg: %s \n", msg)
return nil
}
type SMS struct {
}
func (sms *SMS) Send(msg string) error {
fmt.Printf("SMS Send, msg: %s \n", msg)
return nil
}
func main() {
notify := NewNotify("email")
notify.Send("Hello DI")
}
上述写法中,notify在调用Send方法时,需要感知发送方式,并创建对应的对象。如果新增一种通知类型,Notify的Send方法也需要修改,适配新增的类型。
依赖注入
go 体验AI代码助手 代码解读复制代码// 通过接口抽象发送行为
type Sender interface {
Send(string) error
}
type Notify struct {
sender Sender
}
// 传入依赖项方法设计
func NewNotify(sender Sender) *Notify {
return &Notify{
sender,
}
}
// 使用注入的依赖
func (notify *Notify) Send(msg string) error {
return notify.sender.Send(msg)
}
type Email struct {
}
func (e *Email) Send(msg string) error {
fmt.Printf("Email Send, msg: %s \n", msg)
return nil
}
type SMS struct {
}
func (sms *SMS) Send(msg string) error {
fmt.Printf("SMS Send, msg: %s \n", msg)
return nil
}
func main() {
// 发送msg的依赖,由调入方传入,后续如果新增一种发送方式,sendMsg方法无需修改
notify := NewNotify(&Email{})
notify.Send("Hello DI")
}
改造后,我们抽象了Sender接口,新增一种发送方式时,只需要实现Sender接口,并在NewNotify时,传入即可。Notify不需要感知新增发送方式的存在,解耦了Notify与新增的发送方式。
总结
依赖注入(DI)是一种降低代码耦合度的实现方式,具有比较实用的模板或实现步骤,我们在运用的过程,只需要把握依赖尽量不要由接收者生成,而是通过外部注入这一原则,就能比较容易实现依赖注入,写出松耦合的代码。
作者:举铁敲码
链接:https://juejin.cn/post/7408481531361886244
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。