likes
comments
collection
share

花3分钟写的简易IoC,放在Golang的项目中太好用了~

作者站长头像
站长
· 阅读数 15

为什么说IoC只是一种代码的指导思想

写在前面

和几个小伙伴在字节跳动第五届青训营后端组中,编写的Dousheng项目大作业获奖了。如果是Javer,肯定使用过Spring,那么应该知道IoC,但并不是Spring才能使用IoC。它是一种如何编写代码的指导思想,比如我们的Go Project中,一样使用到了这个思想。来看看我们是如何使用的吧。

首先附上项目地址:

青训营之旅结束了,我给朋友(简称小cher算了🕵️‍♂️🕵️‍♂️)看了我在我Go Project中实现的简易版IOC,实现思路如图所示:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

什么?小cher说她看不懂!!!好吧,我的锅。那我来解释解释,什么是IoC?

注:本文是利用了Ioc的一些思想,实现了简易版的IoC容器,和IoC的原思想并不完全一致。可以小马过河,因人而异~

一、什么是IOC

🕵️‍♂️🕵️‍♂️小cher说,你用一句话解释I一下IoC的核心思想:

一句话解释:将对象利用控制反转的方式,在容器中创建出Bean(项目的依赖对象),并且可以自动为每一个Bean注入所需的依赖。

再用一句话解释简易版IoC:将依赖对象注入容器中后,用控制反转的方式,从IoC容器中获取所需要的依赖。

🎅🎅我还补充了一句:

当然,一般从IoC中取出的依赖,也是为了注入给容器中的其它依赖。

🕵️‍♂️🕵️‍♂️这一下小cher就懵了啊:

什么是控制反转?什么又是依赖?注入之后怎么取出来勒?为什么会有IoC勒?

没关系,咱们慢慢来看,就聊聊天嘛~

(1)什么是依赖注入?

🎅🎅小cher啊,我先跟你说官方一点的啊:

1、官方一点的

DI(Dependency Injection)称为依赖注入。意思是:如果A实例依赖B实例,如下代码所示:

type A struct {
    // A对象依赖对象 B
    b B
}

type B struct {
    name string
}

在程序启动时的时候,会去初始化IoC容器,去初始化对象A的时候,扫描到它需要依赖对象B。IoC会从自身取出B,给A对象中的b字段赋值。

🕵️‍♂️🕵️‍♂️我说完这句话后:小cher说:

这个过程,如果给A赋值的时候,B对象还未初始化呢?那之后如果调用A.b的方法,不就相当于A.nil.xxx了嘛?

是的,所以我们要求,在进行依赖注入的时候,必须要能够在容器中能找到被依赖的对象。


🕵️‍♂️🕵️‍♂️话音刚落,小cher加上了自己的思考,已经理解大概的意思了,他说:那还有没有简单一点的描述?

2、简易版

简单说明了上面的那种解释,再来强化一下理解,因为我们要实现的简易版,依赖注入指的是:

如果将程序刚开始启动时分为两个阶段

  • 阶段一:通过一定的手段,将依赖放入IoC对应的容器中
  • 阶段二:去初始化容器中的对象,也就是给容器中每一个对象的属性赋值。

也就是图中的这个过程:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

🕵️‍♂️🕵️‍♂️图还没看完,小cher说,欧,原来这两个步骤就是依赖注入的过程啊!!!这个懂了,但是我还有个疑惑:你说通过一定的手段。具体指的是什么呢?

(2)怎么注入容器?

是啊,怎么通过一定的手段将依赖放入容器呢?这里有几个可借鉴的手段:可以是配置文件、可以是注解、注释...

🎅🎅撇开上面这句话,小cher啊,如果有一个盒子,让你把一个球放入一个盒子里,你会怎么放入呢?

🕵️‍♂️🕵️‍♂️我啊,我会大致会有两个思路吧:

  1. 自己手动放进去咯
  2. 跟我女朋友说,当我这个盒子打开的时候,她就把球自动放进去~🐶🐶

🎅🎅 ???满脸羡慕,有女朋友真幸福是吧~ 咳咳,先继续来看这个问题:

呀,还挺会思考的勒。主线思路也就是自动放入和手动放入咯,但是我们作为”高级“程序员,会手动操作吗?

所以,我们注入容器的过程,就选择自动注册,那怎么注册呢?既然我这里是Go ProjectGo语言的程序员围过来,其他语言围观,我们来看一段 Go官方的Mysql驱动包 的代码(主要看标注的):

花3分钟写的简易IoC,放在Golang的项目中太好用了~

🎅🎅小cher,相信你猜出我想说什么了吧!

🕵️‍♂️🕵️‍♂️那我浅猜一下?你是不是想说:

在程序刚开始启动时,如果你的启动入口导入了对应的包,那么就会去加载那个包的一些东西。我在网上看到了一副图片,分享给你看看:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

而使用匿名导入,是因为在导入包的地方不需要使用它,只是想去初始化那个包。你看看我说的对吗??

🎅🎅哇偶,可以啊,小cher你是懂我的!!!来,碰个杯🍻我再接着你的思路往下说

我们知道了这个原理,那么我们就可以将要放入容器的操作写到init()函数里,比如下面两段伪代码:

package api

// 这个包有依赖需要放入IoC容器

// ...

func init() {
    // 放入IoC容器的逻辑
    ioc.DI(需要放入的依赖)
}

那么在启动程序的时候:

package main

import (
	_ "test/api" // 去初始化 api 包
)

func main() {
    // 启动程序 
    // ....
}

🕵️‍♂️🕵️‍♂️小cher看完了这两段代码,豁然开朗,原来是这样啊!!!那我想问问,说控制反转,控制怎么就反转了呢?

(2)控制怎么就反转了?

🎅🎅好的,小cher,看这个问题之前,你再回想一下,你在项目中一般是怎么使用依赖对象的?最好可以写点简单的代码给我看看

🕵️‍♂️🕵️‍♂️呐,你看

// 对象A
type A struct {
   id int
   b  *B // 这里依赖 B 对象
}

// 对象B的构造函数
func NewB(name string) *B {
   return &B{name: name}
}

// 对象B
type B struct {
   name string
}

func main() {
   a := A{
      id: 20,
      b:  NewB("ciusyan"), // 自己控制B对象的初始化
   }
   fmt.Println(a.b.name)
}

是啊,你看看,你写的这个,你的A对象依赖B对象,你要是给A对象初始化。你需要自己去写初始化代码。这样我觉得麻烦的地方就是:

  1. 如果你这个类似的代码在100个地方用了,那么你就会写一百遍类似的代码。如果你的参数突然变化了,那么你又要到那用到的一百个地方修改代码
  2. 需要自己理清楚所依赖的对象

🕵️‍♂️🕵️‍♂️也是偶,那我可以封装一下啊!!!哎,不对,你不就是在利用IoC的思想封装吗

是的,我们使用IoC的方式封装后(你先别管具体怎么封装的),初始化的操作,都被放入容器中了。需要使用对象的时候,直接从容器中取出来即可,比如:

func main() {
    // 从IOC中获取A对象
   a := ioc.Get("A")
    // 它是如何初始化A和B的,我们根本不需要关心
   fmt.Println(a.b.name)
}

如上代码所示,我们从IoC中取出了想要使用的对象A,它的内部是如何初始化对象B的,我们根本不用关心,也不必挂念,它到底是用阿猫、还是阿狗来初始化的。

这下对B的控制权,不就反转了吗?从你自己控制,反转成了IoC容器帮你控制

再给你找一副图看看:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

🕵️‍♂️🕵️‍♂️好吧,确实是,用IoC封装后,使用起来好方便啊,都不需要自己管理依赖的对象了!!!再看你开始给我看的图,好像清晰了很多

花3分钟写的简易IoC,放在Golang的项目中太好用了~

相信你看到这里,你大概也知道为什么会出现IoC了,那我们再来总结一下~

二、为什么会有IOC

一句话解释:方便管理项目的依赖的对象。

刚刚所述的,如果很多重复性很大的代码,这个点咱们不重复了叙述了。下面来看看,如果一个对象的依赖很多,那么你可能去理清这个对象的依赖,会很麻烦。

(1)没有IoC时

1、依赖关系混乱

如果你的对象很多,对象间的依赖关系可能很难管理,你可能很难理清楚它们之间的关系。比如下图:

花3分钟写的简易IoC,放在Golang的项目中太好用了~

将上图的依赖关系转换为一段伪代码:

// 对象A
type A struct {
   id int
   b  *B // 这里依赖 B 对象
   c  *C // 这里依赖 C 对象
   d  *D // 这里依赖 D 对象
   e  *E // 这里依赖 E 对象
   f  *F // 这里依赖 F 对象
   g  *G // 这里依赖 G 对象
   // ....
}

func main() {
   
    // 创建G对象
    g := NewG()
    
    // 创建I对象,它依赖G
    i := NewI(g)
        
    // 创建D对象。它依赖I
    d := NewD(i)
    
    // 创建H对象,它依赖D
    h := NewH(d)
    
    // 创建对象A
    a := A{
          id: 20,
          b:  NewB(h), // 自己控制B对象的初始化,B依赖H
          c:  NewC(),  // 自己控制C对象的初始化
          d:  d, 	   // 自己控制D对象的初始化
          e:  NewE(),  // 自己控制E对象的初始化
          f:  NewF(),  // 自己控制F对象的初始化
          g:  g,       // 自己控制G对象的初始化
          // ....
       }
    
    // 使用A对象
    fmt.Println(a.id)
}

🕵️‍♂️🕵️‍♂️小cher看完了这段代码,笑道:之前对象的依赖没那么多,好像确实没多少感觉,看你这个依赖多了后,好像是有点麻烦勒

哈哈,没骗你吧,我这还是随便画的图片,看起来还没那么混乱,你可能还是能理清楚,但是我们再来看一种情况。

2、循环依赖问题

如果是在Go语言中,你每依赖一个对象,就需要导入对应的包,依赖关系混乱可能还能够理清楚,但是如果依赖形成一个环状,你导入包的时候,可能会导致包循环导入 (import circle)的问题。

比如下面这段伪代码:

这是A包的对象,它依赖B包的对象

package A

import "circle/B"

type AImpl struct {
	b *B.BImpl
}

这是B包的对象,它依赖C包的对象

package B

import "circle/C"

type BImpl struct {
	c *C.CImpl
}

这是C包的对象,它依赖A包的对象

package C

import "circle/A"

type CImpl struct {
   a *A.AImpl
}

可以看到这三段代码的 import ,大致是这样的:A -> B,B -> C,C -> A,这样的关系,你使用其中任意一个对象,都会报错。比如这样使用:

package main

import "circle/A"
    
func main() {
   // 使用其中一个对象,都会报错
   _ = A.AImpl{}
}

如果你运行上面的代码,会导致形如下面这样的错误:

package circle
	imports circle/A
	imports circle/B
	imports circle/C
	imports circle/A: import cycle not allowed

再画一张图给你看看,上面的依赖关系是这样的,循环依赖了。

花3分钟写的简易IoC,放在Golang的项目中太好用了~

(2)有IoC之后

看完了上面的两个问题,我想,你多少可能都遇到过上面的问题吧!那来看看有IoC之后呢?

// 定义这些对象的时候,将这些对象全部注入IoC容器中
ioc.DI(A, B, C, D, E, F, G, H)

// 使用对象A
a := ioc.Get("A")

如果站在对象A维护者的角度来看,我们将A对象放入容器中后,正确的给其注入所需的依赖。在外界使用者的眼里,就只需要 ioc.Get() 获取对象来使用即可。外界甚至都可以不用知道这里面是如何初始化的。这不就给使用者一个很大的便利了吗?因为不用花时间去整理依赖关系了~

而且现在依赖都是由IoC管理的,创建者只需要将对象创建好,放入IoC中即可,这样的话,IoC在进行依赖注入的时候,都是从容器里面取依赖,根本不会有循环导入的问题了~

当然啊,并不是说没有IoC就不行啊,但若你还没有想到好的方案,不妨可以试试用IoC容器化的思想去封装一些东西?

🎅🎅小cher,聊到现在,相信你应该了解了IoC的一些核心概念,那咱们开始制作装依赖的容器,看看3分钟能不能写完吧!

三、开始实践了

(0)实现铺垫

一般会用很多种容器,来装不一样的依赖。

🎅🎅小cher,问你个问题,你家里的大衣柜,你不可能把所有衣物,一啪啦的全扔进去吧。

🕵️‍♂️🕵️‍♂️肯定不会啊,要不然到时候取的时候太麻烦了,可以分开管理一下的

是我的话,我可能会把它分成很多个装衣服的容器:有装衬衫的、装领带的、装西装、装裤子...

🎅🎅哈哈哈,是的,既然你也这样认为。那我们这里也可以多做几个容器,用于装不一样的对象,分开管理。就先来实现一个用来装Gin的HTTP服务对象的容器吧,其余的也是类似思路!

这里以在Main方法中注册HTTP的Handler为例,比如现在在main中是这样使用的:

func main() {
    // 1、加载配置文件
    if err := conf.LoadConfigFromToml(configFile); err != nil {
        panic(err)
    }


    // 2、通过Gin启动HTTP服务
    g := gin.Default()
    
    
    // 3、注册对应服务的路由
    
    // 获取UserHandler对象,并且注册路由    
    userService := impl.NewUserServiceImpl()
    userApi := http.NewUserHttpHandler(userService) // UserHandler 依赖 UserService
    userApi.Registry(g)
    
    // 获取VideoHandler对象,并且注册路由
    videoService := impl.NewVideoServiceImpl()
    videoApi := http.NewVideoHttpHandler(videoService) // VideoHandler 依赖 VideoService
    videoApi.Registry(g)
    
    // 依此类推,注册其他服务的路由 ....

    // 4、启动监听TCP连接
    g.Run(conf.C().App.HttpAddr())
}

这里一定要看明白哟,因为我们就是要对这一段代码做改进,这里的逻辑其实也不难:

  1. 读取项目所需配置
  2. 将路由注册到Gin中去监听
  3. 启动HTTP服务,开始监听连接

重点看第二步,我们这里假设就有两个模块,User 和 Video。我们只注册了两次。如果有10个模块的Handler,那么这样的代码就需要写10遍,它们依赖的Service也需要获取十遍。

那我们如何封装呢?跟着我的思路

(1)初阶简易版本

既然这里Handler对象需要我们手动注入依赖的Service对象,那我们能不能先把依赖的控制权,先反转呢?

1、建立IoC包

先建立一个IoC的包,这里面装着依赖的两个Service对象

package ioc

import (
    "github.com/Go-To-Byte/DouSheng/apps/user"
    "github.com/Go-To-Byte/DouSheng/apps/video"
)

// IOC容器:管理所有服务的实例

var (
	UserService user.Service
	VideoService video.Service
    
        // .... 其他依赖
)

注意这里都要用对应Service的接口欧, 我这里就不赘述为什么要面向接口编程了~

2、修改Main的代码

那么我们在main中,就可以这样改进那一段代码了,为了方便,我只保留我们要封装的代码:

func main() {
    
    // 3、注册对应服务的路由
    
    // 将所需的依赖对象的实现,手动注入到IoC包中去。   
    ioc.UserService := impl.NewUserServiceImpl()
    ioc.VideoService := impl.NewVideoServiceImpl()
    
    userApi := http.NewUserHttpHandler()
    userApi.Registry(g)
    
    // 获取VideoHandler对象,并且注册路由
    videoService := impl.NewVideoServiceImpl()
    videoApi := http.NewVideoHttpHandler(videoService) // VideoHandler 依赖 VideoService
    videoApi.Registry(g)
    
    // 依此类推,注册其他服务的路由 ....
    
}

在这里,我们先将IoC中接口所需要的实现,给手动注入进去(给依赖对象赋值)

然后我们直接新建一个对象即可,不需要手动传入这个依赖的Service对象了

3、修改构造函数

那么这两个Handler的初始化的构造方法,就得修改一下了:


// User Handler 的构造函数
func NewUserHttpHandler() *UserHandler {
    if ioc.UserService == nil {
		panic("IOC中依赖为空:UserService")
	}
    
    return &Handler{
            service: ioc.UserService, // 这里依赖IoC中的UserService对象
    }
}

// Video Handler 的构造函数
func NewVideoHttpHandler() *VideoHandler {
    if ioc.VideoService == nil {
		panic("IOC中依赖为空:VideoService")
	}
    
    return &Handler{
            service: ioc.VideoService, // 这里依赖IoC中的VideoService对象
    }
}

在构造函数中初始化的时候,直接从IoC中去获取依赖的Service对象。

4、初阶版总结

小cher看完这个初阶版本,心里有一万只🐏🐏。在初阶版,你可能看到我感觉什么都没做,但是你仔细思考一下,我们的初阶版本

是不是在使用依赖前,将依赖注入进去了?需要使用的时候,是不是直接从IoC中取出来的?别着急,咱继续往下看看。

感觉也没问题,但是其实还有很多问题,比如:

  1. 需要手动注入依赖
  2. 控制并没有完全反转
  3. 注入的依赖很随意
  4. 代码还是不够精简

那我们接着来改进改进,让它看起来更规范些吧~

(2)完整简易版本

1、规范IoC容器的对象的接口

先定义一组接口,想要将对象放入装Gin的HTTP对象的容器中,必须要实现这个接口。

// GinDependency Gin HTTP的服务实例想要注入此容器,必须实现该接口
type GinDependency interface {
	// Init 如何初始化注入此 IoC 的实例
	Init() error
	// Name 注入服务模块的名称
	Name() string
	// Registry 该模块所需要注册的路由
	Registry(r gin.IRoutes)
}

如果你将依赖放入了IoC中,你还得告诉它,要如何初始化,所以需要实现Init()方法。每放入一个依赖,用什么来标识它,所以需要实现Name()方法。又因为是Gin的容器,用于管理Http的Handler的,需要注册路由和它对应的路由函数,所以需要实现Registry(r gin.IRoutes)方法,告诉gin,IoC容器中的实例,要注册哪些路由。

为什么要定义这样的规范呢?当然是为了不用每添加一个模块,就来IoC这里面预定义一个变量呐。所以抽象了一套接口。大家就可以统一使用了。

var (
   // Gin依赖的 IoC 容器
   ginContainer = map[string]GinDependency{}
)

我们统一将这些对象装入一个Map中,你可能会问,为什么要用Map呢?因为方便取出来,我们每放入一个依赖时,给它标注一个唯一的名字,之后需要取出来的时候,通过这个名字就可以取出来了,这种一一对应的关系,用Map映射很方便。

那你可能又会问,Map 是并发不安全的,防止并发问题,不需要加锁?或者使用Sync.Map吗?哈哈,你说的没问题,考虑得还挺周到。但是我们这些依赖对象,是需要在初始化的时候,就装载的。就像程序的配置文件一样,如果一开始启动的时候出现了问题,你还会启动程序吗?所以我们这里在启动后,最多只会读取IoC容器中的内容,并不会写入,所以不用担心并发问题。

2、提供几个方法

方便外界注入和使用依赖,提供几个函数,函数的逻辑也很简单,并且有对应的注释,可以简单看一下。我就不多赘述了。

// GinDI :将依赖注入此容器,Gin DI(Gin Dependency Inject)
func GinDI(dependency GinDependency) {
   dependencyName := dependency.Name()

   // 1、检查服务是否已经被注册
   if _, ok := ginContainer[dependencyName]; ok {
      panic(fmt.Sprintf("[ %s ]服务的依赖已在容器中,请勿重复注入", dependencyName))
   }

   // 2、未注入,放入容器
   ginContainer[dependencyName] = dependency
}

// GetGinDependency 根据模块名称 获取内部服务模块的依赖,外部使用时需自己断言,如:
//
// userGin = ioc.GetGinDependency("user").(user.UserGin)
func GetGinDependency(name string) GinDependency {
   if v, ok := ginContainer[name]; ok {
      return v
   } else {
      panic(fmt.Sprintf("容器中没有此依赖[ %s ]", name))
   }
}

// RegistryGin 注册所有的Gin Http 路由Handler
func RegistryGin(r gin.IRouter) {
  	// 初始化所有对象的路由
	for _, v := range ginApps {
        // 调用它的注册函数
		v.Registry(r)
	}
}

// ExistingGinDependencies 返回Gin HTTP服务依赖的容器中已存在的依赖名称
func ExistingGinDependencies() (apps []string) {
   for k, _ := range ginContainer {
      apps = append(apps, k)
   }
   return
}

但是你还没看到这里面,并没有去调用对应的Init()方法,并没有看到我去初始化这个对象,所以还需要提供一个初始化的函数。又因为我们开头说了,有多个容器,装着不一样的依赖对象,所以我们可以提供一个统一初始化IoC的函数。

3、统一初始化函数

如下代码所示:我们这里提供了三个容器,有内部服务对象的容器、GRPC对象的容器、Gin的Handler对象的容器。我们这里面是用Gin的来举例的,其实每一个容器的实现大同小异。可以看看具体代码~

// InitAllDependencies 用于初始IoC容器中的所有依赖
func InitAllDependencies() error {
   // 初始化内部服务模块依赖
   for _, v := range internalContainer {
      if err := v.Init(); err != nil {
         return err
      }
   }

   // 初始化GRPC服务依赖
   for _, v := range grpcContainer {
      if err := v.Init(); err != nil {
         return err
      }
   }

   // 初始化Gin HTTP服务依赖
   for _, v := range ginContainer {
      if err := v.Init(); err != nil {
         return err
      }
   }

   return nil
}

有了这个函数之后,我们来看看如何使用我们的IoC容器吧

4、使用

  • 先看看是如何将依赖对象放入容器的:
// 用于注入IOC中
var handler = &Handler{}

// Handler 通过一个实体类,把内部接口用HTTP暴露出去【控制层Controller】
type Handler struct {
   service user.UserService
   log     *zap.SugaredLogger
}

// Registry 用于注册Handler所需要暴露的路由
func (h *Handler) Registry(r route.IRoutes) {
   r.POST("/register/", h.Register)
   r.POST("/login/", h.Login)
   r.GET("/", h.GetUserInfo)
}

// Init 初始化Handler对象
func (h *Handler) Init() error {
   // 从内部服务的IOC中获取UserServiceImpl实例
   h.service = ioc.GetInternalDependency(user.AppName).(user.Service)

   // 如何初始化日志对象
   h.log = zap.S().Named("USER HTTP")
   return nil
}

func (h *Handler) Name() string {
   return user.AppName // user
}

func init() {	
    // 将此Gin服务注入IOC中
    ioc.GinDI(handler)
}

User模块的Handler对象,实现了放入Gin容器的接口,将其放入Gin的IoC容器中GinDI()方法。并且告诉了,如何初始化此对象Init()方法、注册什么路由Registry()方法、用什么名字标识自己Name()方法

但是我们这里仅仅是注册好了。并没有人来初始化这个包哎,什么时候注入呢?

  • 什么时候注入

刚刚小cher说的,我们可以通过导包的方式,来执行每个文件中的init()方法,也就是形如这样的

import (
	// 将对象放入内部服务的IoC中
        _ "github.com/Go-To-Byte/DouSheng/apps/user/impl" // UserService
        _ "github.com/Go-To-Byte/DouSheng/apps/video/impl" // VideoService
    
        // 将对象放入Gin服务的IoC中
	_ "github.com/Go-To-Byte/DouSheng/apps/user/http" // UserHandler 
	_ "github.com/Go-To-Byte/DouSheng/apps/video/http" // VideoHandler
        
        // ...
)

可是在哪里导入呢?当然,我们可以选择在main方法中,编写上面的代码,这没问题。可是如果有很多这样的依赖需要被注入到IoC中,这里这里就有太多这样的导入代码了。

我们可以将这些导包代码打包放入一个包中,之后在main方法中,就可以只导入那一个包了,具体代码下面一起贴。

导包没有问题了,也就是依赖都放入IoC中了,该何时初始化呢?

  • Main方法的代码

当然是在main方法中初始化呐,那我直接贴main方法的代码了~

package main

import (
    	// 在这个包里,统一将依赖放入IoC容器中
	_ "github.com/Go-To-Byte/DouSheng/apps/all"
    	// ...
)

func main() {
    // 1、加载配置文件
    if err := conf.LoadConfigFromToml(configFile); err != nil {
        panic(err)
    }

    // 2、初始化IoC的依赖
    if err := ioc.InitAllDependencies(); err != nil {
        return err
    }

    // 3、通过注册路由及对应的路由函数给Gin,并启动HTTP服务
    g := gin.Default()
    ioc.Registry(g)

    // 4、启动监听TCP连接
    g.Run(conf.C().App.HttpAddr())
}

通过一套操作下来,每增加一个模块,Main这里终于不用每次都去 New 对象、注册对应模块的路由了。

增加新模块时,只需要在那边将依赖放入到对应的IoC中,并且在去 all 里面导入所需要的包即可。

  • 编写单元测试

而且,有了IoC之后,编写单元测试的代码变得极为容易,比如我给你编写一个:

package impl_test

import (
   // 驱动加载所有需要放入IOC的实例
   _ "github.com/Go-To-Byte/DouSheng/user_center/common/all"
    // ...
)

var (
   service user.ServiceServer
)


func TestLogin(t *testing.T) {
   should := assert.New(t)
    
   u := user.NewLoginAndRegisterRequest()
   // 调用业务方法
   token, err := service.Login(context.Background(), u)

   if should.NoError(err) {
      t.Log(token)
   }
}

func init() {
   // 加载配置文件(或者可以从环境变量中读取)
   if err := conf.LoadConfigFromToml("../../../etc/config.toml"); err != nil {
      panic(err)
   }

   // 初始化IOC容器
   if err := ioc.InitAllDependencies(); err != nil {
      panic(err)
   }

   // 从IOC中获取接口实现
   service = ioc.GetGrpcDependency(user.AppName).(user.ServiceServer)
}

需要测试哪个模块,直接从IoC中获取对应的依赖,不用直接暴露接口的实现。

至此,我们简易版的IoC已经编写完成了,基本完成了需求。当然,如果你有个性化需求,你还可以继续拓展IoC,比如统一添加一些中间件(传入一些切片)...

具体的代码,可以看看我们的项目中:项目IoC相关代码,是如何使用的。

所以说,IoC只是一种如何编写代码的指导思想,在很多地方都可以使用它来封装代码。如果文章对你有帮助,不妨点赞收藏使用起来~