Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bean/copier: ReflectCopier 实现 #45

Closed
flycash opened this issue Aug 23, 2022 · 3 comments · Fixed by #47
Closed

bean/copier: ReflectCopier 实现 #45

flycash opened this issue Aug 23, 2022 · 3 comments · Fixed by #47

Comments

@flycash
Copy link
Contributor

flycash commented Aug 23, 2022

仅限中文

使用场景

在实际业务中,我们经常会将系统分成很多层,在不同层会定义不同的实体。例如在 DAO 层有 PO(或者 Entity),在跨端调用的时候有 DTO,在和前端对接的时候有 VO。

大多数情况下,我们需要在这些实体进行转换,例如 DTO 转 PO 等。但是这种代码很呆板,因为仅仅是一个个字段赋值过去,所以在业务代码里面就会充斥着这种转化代码。

因此我们可以考虑设计一个复制器,完成不同类型直接的转换

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

Java BeanUtils

最为有名的应该是 Java 里面提供的 bean utils 依赖,里面支持在 bean 之间进行数据复制。同时 Java 还允许深度定制化复制的行为,例如允许忽略特定的字段,或者指定某个字段映射到另外一个字段。

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

反射

也就是我们这里采用的方案,即一个个字段使用反射复制过去。这里要考虑复杂类型字段,以及组合。

代码生成

代码生成技术是另外一种可行的方案。例如说我们有两个结构体:

type SimpleSrc struct {
	Name    string
	Age     *int
	Friends []string
}

type SimpleDst struct {
	Name    string
	Age     *int
	Friends []string
}

为这两个结构体生成方法:

func From(src SimpleSrc) SimpleDst {
     return SimpleDst {
             Name: src.Name
            // ...
    }
}

这种方案的难点在于,SimpleSrc 和 SimpleDst 处于不同的层次。在实际中,它们基本上分属不同的包,因此我们需要额外解决引入包的问题。这是一个很棘手的问题。

其它

任何你觉得有利于解决问题的补充说明

你可以考虑分成好几个合并请求,也可以一个合并请求就支持完。

接口定义和类型定义,以及方法行为特征我都已经定义好了,在:https://github.com/gotomicro/ekit/blob/dev/bean/copier/reflect_copier.go

要求你必须:

  • 完善的测试用例
  • benchmark 测试:简单结构体,组合,或者复杂类型字段都需要有单独的 benchmark 测试

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

@longyue0521
Copy link
Collaborator

@flycash

现有需求来自一下几段代码:

// ekit/ben/copier/copy.go
// Copier 复制数据
// 1. 深拷贝亦或是浅拷贝,取决于具体的实现。每个实现都要声明清楚这一点;
// 2. Src 和 Dst 都必须是普通的结构体,支持组合
// 3. 只复制公共字段
// 这种设计设计,即使用 *Src 和 *Dst 可能加剧内存逃逸
type Copier[Src any, Dst any] interface {
	// CopyTo 将 src 中的数据复制到 dst 中
	CopyTo(src *Src, dst *Dst) error
	// Copy 将创建一个 Dst 的实例,并且将 Src 中的数据复制过去
	Copy(src *Src) (*Dst, error)
}
// reflectCopier 基于反射的实现
// reflectCopier 是浅拷贝
type reflectCopier[Src any, Dst any] struct {
        // ....
}
// CopyTo 执行复制
// 执行复制的逻辑是:
// 1. 按照字段的映射关系进行匹配
// 2. 如果 Src 和 Dst 中匹配的字段,其类型是基本类型(及其指针)或者内置类型(及其指针),并且类型一样,则直接用 Src 的值
// 3. 如果 Src 和 Dst 中匹配的字段,其类型都是结构体,或者都是结构体指针,那么会深入复制
// 4. 否则,返回类型不匹配的错误
// TODO: 支持不同类型之间的转换
func (r *reflectCopier[Src, Dst]) CopyTo(src *Src, dst *Dst) error {
       // ....
}

需求澄清

  1. “公共字段”的定义是什么?或两个结构体中的某个字段是否“匹配”的定义是什么?

现阶段的理解是,两个结构体中某一字段名称、类型及相对位置均相同,即为匹配/公共字段

type Src struct {
      A string
      b int
      c bool
      D int32
      E error
      F user
}

type Dst struct {
     A string
     b int
     c *bool
     D string
     E  error
     F  *user
}
type user struct {}

匹配字段为A,b,E
不匹配字段为c,D
F是否认定为匹配?

  1. 非导出字段如何处理? 是否忽略?

如果忽略非导出字段,上例中匹配字段为A,E;不匹配字段为D

  1. 不匹配字段的处理细节

通常来说会忽略不匹配字段,但有一种特殊情况就是两个类型没有任何公共/匹配字段是否应该报错?

  1. API设计,我为NewReflectCopier添加了error,用户按照如下代码使用可以吗?
copier, err := NewReflectCopier[Src, Dst]()
if err != nil {
      // 至少要有一个匹配字段
      // Src/Dst类型非法等
      // Src/Dst中某字段类型为多级指针
}

err = copier.CopyTo(......)
if err != nil {
     // 复制时发生的错误
} 

@flycash flycash linked a pull request Aug 28, 2022 that will close this issue
@flycash
Copy link
Contributor Author

flycash commented Aug 28, 2022

公共字段我写错了,应该是公开字段。

  1. 同类型的指针和非指针,在目前这位同学的设计里面是不匹配的。你可以尝试改进这个东西,去除”匹配“的概念。也就是我们引入 converter 的概念。例如,对于一些字段来说,用户可以要求 user 可以被转化过去 *user 甚至 **user 等。以及基本类型之间的转换。可以尝试给用户更加强的控制手段
  2. 非导出字段直接忽略,不管
  3. 两个类型没有任何匹配字段可以报错
  4. 可以,应该是我忘记加了,按照我的使用习惯,我永远都要有 error

@flycash
Copy link
Contributor Author

flycash commented Aug 28, 2022

接下来还有两个方向的优化点:

  • 一个是自定义字段映射关系:例如忽略某个字段,组合的话用 "A.B.C"这样来指定忽略什么;例如我有一个字段叫做 User1 我映射过去 User2 上;
  • 不同类型的字段直接的转换:例如基本类型的转化,指针和非指针的转换,string 其它基本类型,以及 []byte 的转换

还可以考虑的是,提供校验手段:

  • 例如当某个字段映射过去另外一个字段的时候,可以顺手校验一下字段对不对。不过这个需要校验器模块的支持,我记得开源有,但是我还没去关注它的实现好不好

This was referenced Sep 6, 2022
Closed
@flycash flycash closed this as completed Sep 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants