探究 Go 的高级特性之 【领域驱动设计中篇】
IOC(Inversion of Control)即“控制反转”,是一种设计模式,用于实现松耦合和模块化设计。在IOC中,控制权被交给框架或容器,它们负责管理应用程序的对象和依赖关系
这是一种思想
而实现IOC的方式就是通过DI方式。简单来说 DI 就是通过参数或者属性的方式注入所需要的依赖项。
概述和术语
DI 的字面意思是注入你的依赖。依赖关系可以是影响逻辑行为或结果的任何事物。一些常见的例子是:
- 其他服务。使您的代码更加模块化,减少重复代码并且更易于测试。
- 配置。例如数据库密码、API URL 端点等。
- 系统或环境状态。例如时钟或文件系统。在编写依赖于时间或随机数据的测试时,这一点非常重要。
- 外部 API 的存根。以便在测试期间可以在系统内模拟 API 请求,以保持稳定和快速。
一些术语:
- 服务是类的实例 。之所以称为服务,是因为它通常通过名称而不是类型来引用。例如
Emailer
是一个服务的名称,但它是一个SendEmail
. 我们可以更改服务的底层实现。只要它具有相同的接口,我们就不需要重命名该服务。 - 容器是服务的集合 。 服务是延迟加载的,只有在从容器请求时才初始化。
- 单例是初始化一次,但可以重复使用多次的实例。
在 Go 中实现 DI 可以使用依赖接口或者函数类型,也可以使用第三方依赖注入框架,比如 Google 实现的 Wire 框架等。使用依赖接口或函数类型的方式相对简单,且不需要任何第三方依赖。它将依赖的选择和注入的方式交给调用方处理,从而增加了代码的灵活性和可测试性。
简单的例子
下面是一个简单的例子,通过使用依赖接口实现了 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参数,会被自动注入:
属性注入
接下来我们看下product.Repository
的实现:
InitProduct
函数会执行注册这个实现到product.Repository
接口中
在这个代码示例中,
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容器
如果当前这个结构体是通过container容器取出来的,就会去查找这个对象(结构体)字段中,是否有接口类型的字段,并且是已注册到container中的。就会启用属性注入。
// 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
}
转载自:https://juejin.cn/post/7247428110165016613