likes
comments
collection
share

探究 Go 的高级特性之 【领域驱动设计中篇】

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

IOC(Inversion of Control)即“控制反转”,是一种设计模式,用于实现松耦合和模块化设计。在IOC中,控制权被交给框架或容器,它们负责管理应用程序的对象和依赖关系

这是一种思想

而实现IOC的方式就是通过DI方式。简单来说 DI 就是通过参数或者属性的方式注入所需要的依赖项

概述和术语

DI 的字面意思是注入你的依赖。依赖关系可以是影响逻辑行为或结果的任何事物。一些常见的例子是:

  1. 其他服务。使您的代码更加模块化,减少重复代码并且更易于测试。
  2. 配置。例如数据库密码、API URL 端点等。
  3. 系统或环境状态。例如时钟或文件系统。在编写依赖于时间或随机数据的测试时,这一点非常重要。
  4. 外部 API 的存根。以便在测试期间可以在系统内模拟 API 请求,以保持稳定和快速。

一些术语:

  • 服务是类的实例 。之所以称为服务,是因为它通常通过名称而不是类型来引用。例如Emailer是一个服务的名称,但它是一个SendEmail. 我们可以更改服务的底层实现。只要它具有相同的接口,我们就不需要重命名该服务。
  • 容器是服务的集合 服务是延迟加载的,只有在从容器请求时才初始化。
  • 是初始化一次,但可以重复使用多次的实例。

在 Go 中实现 DI 可以使用依赖接口或者函数类型,也可以使用第三方依赖注入框架,比如 Google 实现的 Wire 框架等。使用依赖接口或函数类型的方式相对简单,且不需要任何第三方依赖。它将依赖的选择和注入的方式交给调用方处理,从而增加了代码的灵活性和可测试性。

探究 Go 的高级特性之 【领域驱动设计中篇】

简单的例子

下面是一个简单的例子,通过使用依赖接口实现了 DI。


type Logger interface {

  Log(m string)

}

type DBLogger struct {

  db *sql.DB

}

func (l *DBLogger) Log(m string) {

  _, err := l.db.Exec("INSERT INTO log (message) VALUES (?)", m)

  if err != nil {
      panic(err)
  }
}


type App struct {
  logger Logger
}

 
func (a *App) Run() {
  a.logger.Log("The App has been run.")
}


func main() {

  db, err := sql.Open("mysql", "user:password@tcp(database)/database")

  // error handling

  logger := &DBLogger{db}

  app := &App{logger}

  app.Run()

}

在上面的代码中,我们定义了Logger 接口和DBLogger 结构体,DBLogger 结构体实现了 Logger 接口。 App 结构体包含了一个 logger 参数。在运行时,我们注入了一个 DBLogger 对象到 App 结构体中,实现了依赖注入。

当我们运行 App.Run() 方法时,方法内部通过调用logger.Log()方法,从而调用了 DBLogger 结构体的 Log() 方法,将信息日志到数据库中。

通过使用依赖接口来实现 DI,我们将所需要使用的依赖项交给了调用方,从而增加了代码的灵活性和可测试性。即使需要在不同的地方使用不同的 Logger 实现,我们也只需要实现 Logger 接口即可。

这是个简单的例子 实际项目中,我们如何在ddd的思想中使用DI呢?

项目实战

注入的方式又分为:

  • 构造函数注入
  • 方法入参注入
  • 属性注入

在GO中,并没有构造函数注入(实际上是用函数的方式来模拟)。所以我们这里主要是讲解后两种。

使用到的框架:farseer-go 如果可以的话,请大家给farseer-go 1个star

项目开源代码:github

框架官方文档:文档

方法入参注入

我们来看下这个代码示例


	fs.Initialize[StartupModule]("kol")

	//webapi.Area("/api/1.0/", func() {
	//	// 商品分类列表
	//	// get http://localhost:8888/api/1.0/cate/list
	//	webapi.RegisterPOST("/mini/hello1", Hello1)
	//})
	webapi.Area("/api/1.0/", func() {
		// 商品分类列表

		// 商品列表
		// get http://localhost:8888/api/1.0/product/list?pageIndex=1&pageSize=3&cateId=0
		webapi.RegisterGET("/product/list", productApp.ToList, "cateId", "pageSize", "pageIndex", "", "")
	})

从上方的代码,我们注册了一个productApp.ToEntity 动态API。

// webapi注入请参考:https://farseer-go.gitee.io/#/web/webapi/container
func ToList(cateId, pageSize, pageIndex int, productRepository product.Repository, stockRepository stock.Repository) collections.PageList[DTO] {
	if pageIndex == 0 {
		pageIndex = 1
	}
	if pageSize == 0 {
		pageSize = 10
	}
	// 从仓储接口获取数据
	lstDO := productRepository.ToPageList(cateId, pageSize, pageIndex)

	// 转成PageList
	var lstDTO collections.PageList[DTO]
	lstDO.MapToPageList(&lstDTO)

	stocks := stockRepository.GetAll()
	for i := 0; i < lstDTO.List.Count(); i++ {
		item := lstDTO.List.Index(i)
		item.Stock = stocks[item.Id]
		lstDTO.List.Set(i, item)
	}
	return lstDTO
}

ToEntity接收三种入参,int、product.Repository(接口)、stock.Repository(接口)。当前端请求这个接口时,repository参数,会被自动注入:

探究 Go 的高级特性之 【领域驱动设计中篇】

属性注入

接下来我们看下product.Repository的实现:

探究 Go 的高级特性之 【领域驱动设计中篇】 InitProduct函数会执行注册这个实现到product.Repository接口中

探究 Go 的高级特性之 【领域驱动设计中篇】 在这个代码示例中,StockRepository结构体中包含了:Redis字段,并设置标签: inject:"default"

同时InitStock函数会执行注册这个实现到StockRepository接口中。

func InitStock() {
   container.Register(func() stock.Repository {
      return &StockRepository{}
   })
}

type StockRepository struct {
   Redis redis.IClient `inject:"default"` // 使用farseer.yaml的Redis.default配置节点,并自动注入
}

func (receiver *StockRepository) Get(productId int64) int {
   stockVal, _ := receiver.Redis.HashGet(stockKey, strconv.FormatInt(productId, 10))
   return parse.Convert(stockVal, 0)
}

func (receiver *StockRepository) GetAll() map[int64]int {
   all, _ := receiver.Redis.HashGetAll(stockKey)
   result := make(map[int64]int)
   for k, v := range all {
      result[parse.Convert(k, int64(0))] = parse.Convert(v, 0)
   }
   return result
}

什么时候这个属性会被注入?

首先需要在项目启动的时候将repository注册到container容器

探究 Go 的高级特性之 【领域驱动设计中篇】

如果当前这个结构体是通过container容器取出来的,就会去查找这个对象(结构体)字段中,是否有接口类型的字段,并且是已注册到container中的。就会启用属性注入。

探究 Go 的高级特性之 【领域驱动设计中篇】

// MapToParams 将map转成入参值
func (receiver *HttpRoute) MapToParams(mapVal map[string]any) []reflect.Value {
   // dto模式
   if receiver.RequestParamIsModel {
      // 第一个参数,将json反序列化到dto
      param := receiver.RequestParamType.First()
      paramVal := reflect.New(param).Elem()
      for i := 0; i < param.NumField(); i++ {
         field := param.Field(i)
         if !field.IsExported() {
            continue
         }
         key := field.Tag.Get("json")
         if key == "" {
            key = strings.ToLower(field.Name)
         }
         kv, exists := mapVal[key]
         if exists {
            defVal := paramVal.Field(i).Interface()
            paramVal.FieldByName(field.Name).Set(reflect.ValueOf(parse.Convert(kv, defVal)))
         }
      }
      returnVal := []reflect.Value{paramVal}

      // 第2个参数起,为interface类型,需要做注入操作
      for i := 1; i < receiver.RequestParamType.Count(); i++ {
         val := container.ResolveType(receiver.RequestParamType.Index(i))
         returnVal = append(returnVal, reflect.ValueOf(val))
      }
      return returnVal
   }

   // 多参数
   lstParams := make([]reflect.Value, receiver.RequestParamType.Count())
   for i := 0; i < receiver.RequestParamType.Count(); i++ {
      fieldType := receiver.RequestParamType.Index(i)
      var val any
      // interface类型,则通过注入的方式
      if fieldType.Kind() == reflect.Interface {
         val = container.ResolveType(fieldType)
      } else {
         val = reflect.New(fieldType).Elem().Interface()
         if receiver.ParamNames.Count() > i {
            paramName := strings.ToLower(receiver.ParamNames.Index(i))
            paramVal := mapVal[paramName]
            val = parse.Convert(paramVal, val)

            // 当实际只有一个接收参数时,不需要指定参数
         } else if receiver.ParamNames.Count() == 0 && len(mapVal) == 1 {
            for _, paramVal := range mapVal {
               val = parse.Convert(paramVal, val)
            }
         }
      }
      lstParams[i] = reflect.ValueOf(val)
   }
   return lstParams
}