将 Kotlin 数据对象映射到数据对象的更好方法

新手上路,请多包涵

我想将一些“数据”类对象转换/映射到类似的“数据”类对象。例如,Web 表单类到数据库记录类。

 data class PersonForm(
    val firstName: String,
    val lastName: String,
    val age: Int,
    // maybe many fields exist here like address, card number, etc.
    val tel: String
)
// maps to ...
data class PersonRecord(
    val name: String, // "${firstName} ${lastName}"
    val age: Int, // copy of age
    // maybe many fields exist here like address, card number, etc.
    val tel: String // copy of tel
)

我在 Java 中使用 ModelMapper 进行此类工作,但它不能使用,因为数据类是最终的(ModelMapper 创建 CGLib 代理来读取映射定义)。当我们打开这些类/字段时,我们可以使用 ModelMapper,但我们必须手动实现“数据”类的功能。 (参见 ModelMapper 示例: https ://github.com/jhalterman/modelmapper/blob/master/examples/src/main/java/org/modelmapper/gettingstarted/GettingStartedExample.java)

如何在 Kotlin 中映射此类“数据”对象?

更新: ModelMapper 自动映射具有相同名称的字段(如 tel -> tel),无需映射声明。我想用 Kotlin 的数据类来做。

更新: 每个类的目的取决于什么样的应用程序,但这些可能位于应用程序的不同层。

例如:

  • 从数据库(数据库实体)到 HTML 表单数据(模型/视图模型)的数据
  • REST API 结果到数据库的数据

这些类相似,但不相同。

由于这些原因,我想避免正常的函数调用:

  • 这取决于参数的顺序。具有许多具有相同类型(如字符串)的字段的类的函数很容易被破坏。
  • 许多声明是必需的,尽管大多数映射可以通过命名约定来解决。

当然,一个具有类似功能的库是有意的,但也欢迎提供 Kotlin 功能的信息(如在 ECMAScript 中传播)。

原文由 sunnyone 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 888
1 个回答
  1. 最简单(最好?):
    fun PersonForm.toPersonRecord() = PersonRecord(
           name = "$firstName $lastName",
           age = age,
           tel = tel
   )

  1. 反思(不是很好的表现):
    fun PersonForm.toPersonRecord() = with(PersonRecord::class.primaryConstructor!!) {
       val propertiesByName = PersonForm::class.memberProperties.associateBy { it.name }
       callBy(args = parameters.associate { parameter ->
           parameter to when (parameter.name) {
               "name" -> "$firstName $lastName"
               else -> propertiesByName[parameter.name]?.get(this@toPersonRecord)
           }
       })
   }

  1. 缓存反射(性能还行,但不如#1 快):
    open class Transformer<in T : Any, out R : Any>
   protected constructor(inClass: KClass<T>, outClass: KClass<R>) {
       private val outConstructor = outClass.primaryConstructor!!
       private val inPropertiesByName by lazy {
           inClass.memberProperties.associateBy { it.name }
       }

       fun transform(data: T): R = with(outConstructor) {
           callBy(parameters.associate { parameter ->
               parameter to argFor(parameter, data)
           })
       }

       open fun argFor(parameter: KParameter, data: T): Any? {
           return inPropertiesByName[parameter.name]?.get(data)
       }
   }

   val personFormToPersonRecordTransformer = object
   : Transformer<PersonForm, PersonRecord>(PersonForm::class, PersonRecord::class) {
       override fun argFor(parameter: KParameter, data: PersonForm): Any? {
           return when (parameter.name) {
               "name" -> with(data) { "$firstName $lastName" }
               else -> super.argFor(parameter, data)
           }
       }
   }

   fun PersonForm.toPersonRecord() = personFormToPersonRecordTransformer.transform(this)

  1. 在地图中存储属性
   data class PersonForm(val map: Map<String, Any?>) {
       val firstName: String   by map
       val lastName: String    by map
       val age: Int            by map
       // maybe many fields exist here like address, card number, etc.
       val tel: String         by map
   }

   // maps to ...
   data class PersonRecord(val map: Map<String, Any?>) {
       val name: String    by map // "${firstName} ${lastName}"
       val age: Int        by map // copy of age
       // maybe many fields exist here like address, card number, etc.
       val tel: String     by map // copy of tel
   }

   fun PersonForm.toPersonRecord() = PersonRecord(HashMap(map).apply {
       this["name"] = "${remove("firstName")} ${remove("lastName")}"
   })

原文由 mfulton26 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题