Kotlin - 如果不为空,则使用修改后的 Obj 道具覆盖 Obj 道具

Kotlin - Overwrite Obj Props With Modified Obj Props if Not Null

TL;DR:

如何减少冗余(任何有用的方法)?

if (personModification.firstName != null) {person.firstName = personModification.firstName}
if (personModification.lastName != null) {person.lastName = personModification.lastName}
if (personModification.job != null) {person.job = personModification.job}

长版:我有一个简单的问题。我有一个 class Person:

class Person (val firstName: String?, 
              val lastName: String?, 
              val job: String?)

我有一个 class 叫做 PersonModification:

class PersonModification(val firstName: String?, 
                         val lastName: String?, 
                         val job: String?)

任务是用 PersonModification 值覆盖任何 Person 属性 值,如果 PersonModification 属性 不是 null .如果你关心的话,这背后的业务逻辑是一个 API 端点,它修改 Person 并将 PersonModification 作为参数(但可以更改所有或任何属性,所以我们不想用空值覆盖有效的旧值)。解决方案如下所示。

if (personModification.firstName != null) {person.firstName = personModification.firstName}
if (personModification.lastName != null) {person.lastName = personModification.lastName}
if (personModification.job != null) {person.job = personModification.job}

有人告诉我这是多余的(我同意)。解决方案伪代码如下所示:

foreach(propName in personProps){
  if (personModification["propName"] != null) {person["propName"] = personModification["propName"]}
}

当然,这不是 JavaScript,所以没那么容易。我的反射解决方案在下面,但是 imo,最好有冗余而不是在这里进行反射。我还有哪些删除冗余的其他选项?


反思:

package kotlin.reflect;

class Person (val firstName: String?, 
              val lastName: String?, 
              val job: String?)

class PersonModification(val firstName: String?, 
                         val lastName: String?, 
                         val job: String?)

// Reflection - a bad solution. Impossible without it.
//
inline fun <reified T : Any> Any.getThroughReflection(propertyName: String): T? {
    val getterName = "get" + propertyName.capitalize()
    return try {
        javaClass.getMethod(getterName).invoke(this) as? T
    } catch (e: NoSuchMethodException) {
        null
    }
}

fun main(args: Array<String>) {

var person: Person = Person("Bob","Dylan","Artist")
val personModification: PersonModification = PersonModification("Jane","Smith","Placeholder")
val personClassPropertyNames = listOf("firstName", "lastName", "job")

for(properyName in personClassPropertyNames) {
    println(properyName)
    val currentValue = person.getThroughReflection<String>(properyName)
    val modifiedValue = personModification.getThroughReflection<String>(properyName)
    println(currentValue)
    if(modifiedValue != null){
        //Some packages or imports are missing for "output" and "it"
        val property = outputs::class.memberProperties.find { it.name == "firstName" }
        if (property is KMutableProperty<*>) {
            property.setter.call(person, "123")
        }
    }
})
}

你可以在这里复制粘贴到运行它:https://try.kotlinlang.org/

编写一个 5 行帮助程序来执行此操作应该非常简单,它甚至支持复制每个匹配项 属性 或仅选择属性。

尽管如果您正在编写 Kotlin 代码并大量使用数据 classes 和 val(不可变属性),它可能没有用。看看:

fun <T : Any, R : Any> T.copyPropsFrom(fromObject: R, skipNulls: Boolean = true, vararg props: KProperty<*>) {
  // only consider mutable properties
  val mutableProps = this::class.memberProperties.filterIsInstance<KMutableProperty<*>>()
  // if source list is provided use that otherwise use all available properties
  val sourceProps = if (props.isEmpty()) fromObject::class.memberProperties else props.toList()
  // copy all matching
  mutableProps.forEach { targetProp ->
    sourceProps.find {
      // make sure properties have same name and compatible types 
      it.name == targetProp.name && targetProp.returnType.isSupertypeOf(it.returnType) 
    }?.let { matchingProp ->
      val copyValue = matchingProp.getter.call(fromObject);
      if (!skipNulls || (skipNulls && copyValue != null)) {
        targetProp.setter.call(this, copyValue)
      }
    }
  }
}

这种方法使用反射,但它使用非常轻量级的 Kotlin 反射。我没有计时,但它应该 运行 几乎与手动复制属性的速度相同。

它还使用 KProperty 而不是字符串来定义属性的子集(如果您不想复制所有属性),因此它具有完整的重构支持,所以如果您重命名 属性 在 class 上,您不必寻找字符串引用来重命名。

默认情况下它会跳过空值,或者您可以将 skipNulls 参数切换为 false(默认为 true)。

现在给出 2 classes:

data class DataOne(val propA: String, val propB: String)
data class DataTwo(var propA: String = "", var propB: String = "")

您可以执行以下操作:

  var data2 = DataTwo()
  var data1 = DataOne("a", "b")
  println("Before")
  println(data1)
  println(data2)
  // this copies all matching properties
  data2.copyPropsFrom(data1)
  println("After")
  println(data1)
  println(data2)
  data2 = DataTwo()
  data1 = DataOne("a", "b")
  println("Before")
  println(data1)
  println(data2)
  // this copies only matching properties from the provided list 
  // with complete refactoring and completion support
  data2.copyPropsFrom(data1, DataOne::propA)
  println("After")
  println(data1)
  println(data2)

输出将是:

Before
DataOne(propA=a, propB=b)
DataTwo(propA=, propB=)
After
DataOne(propA=a, propB=b)
DataTwo(propA=a, propB=b)
Before
DataOne(propA=a, propB=b)
DataTwo(propA=, propB=)
After
DataOne(propA=a, propB=b)
DataTwo(propA=a, propB=)

这可以在不使用委托属性反射的情况下解决。参见:https://kotlinlang.org/docs/reference/delegated-properties.html

class Person(firstName: String?,
             lastName: String?,
             job: String?) {
    val map = mutableMapOf<String, Any?>()
    var firstName: String? by map
    var lastName: String? by map
    var job: String? by map

    init {
        this.firstName = firstName
        this.lastName = lastName
        this.job = job
    }
}

class PersonModification(firstName: String?,
                         lastName: String?,
                         job: String?) {
    val map = mutableMapOf<String, Any?>()
    var firstName: String? by map
    var lastName: String? by map
    var job: String? by map

    init {
        this.firstName = firstName
        this.lastName = lastName
        this.job = job
    }
}


fun main(args: Array<String>) {

    val person = Person("Bob", "Dylan", "Artist")
    val personModification1 = PersonModification("Jane", "Smith", "Placeholder")
    val personModification2 = PersonModification(null, "Mueller", null)

    println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")

    personModification1.map.entries.forEach { entry -> if (entry.value != null) person.map[entry.key] = entry.value }
    println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")

    personModification2.map.entries.forEach { entry -> if (entry.value != null) person.map[entry.key] = entry.value }
    println("Person: firstName: ${person.firstName}, lastName: ${person.lastName}, job: ${person.job}")


}

您可以为此创建一个不错的特征,您将能够申请任何修改class,您可能拥有:

interface Updatable<T : Any> {

    fun updateFrom(model: T) {
        model::class.java.declaredFields.forEach { modelField ->
            this::class.java.declaredFields
                    .filter { it.name == modelField.name && it.type == modelField.type }
                    .forEach { field ->
                        field.isAccessible = true
                        modelField.isAccessible = true
                        modelField.get(model)?.let { value ->
                            field.set(this, value)
                        }
                    }
        }
    }
}

用法:

data class Person(val firstName: String?,
                  val lastName: String?,
                  val job: String?) : Updatable<PersonModification>

data class PersonModification(val firstName: String?,
                              val lastName: String?,
                              val job: String?)

那你可以试试看:

fun main(args: Array<String>) {

    val person = Person(null, null, null)

    val mod0 = PersonModification("John", null, null)
    val mod1 = PersonModification(null, "Doe", null)
    val mod2 = PersonModification(null, null, "Unemployed")

    person.updateFrom(mod0)
    println(person)

    person.updateFrom(mod1)
    println(person)

    person.updateFrom(mod2)
    println(person)
}

这将打印:

Person(firstName=John, lastName=null, job=null)
Person(firstName=John, lastName=Doe, job=null)
Person(firstName=John, lastName=Doe, job=Unemployed)

已经有很多人提出了他们的解决方案。但我想再提供一个:

jackson 中有一些有趣的功能,您可以尝试合并 json。因此,您可以将 src 对象与 PersonModification

的反序列化版本合并

有了它,可以做这样的事情:

class ModificationTest {
    @Test
    fun test() {
        val objectMapper = jacksonObjectMapper().apply {
            setSerializationInclusion(JsonInclude.Include.NON_NULL)
        }

        fun Person.merge(personModification: PersonModification): Person = run {
            val temp = objectMapper.writeValueAsString(personModification)

            objectMapper.readerForUpdating(this).readValue(temp)
        }

        val simplePerson = Person("firstName", "lastName", "job")

        val modification = PersonModification(firstName = "one_modified")
        val modification2 = PersonModification(lastName = "lastName_modified")

        val personAfterModification1: Person = simplePerson.merge(modification)
        //Person(firstName=one_modified, lastName=lastName, job=job)
        println(personAfterModification1)

        val personAfterModification2: Person = personAfterModification1.merge(modification2)
        //Person(firstName=one_modified, lastName=lastName_modified, job=job)
        println(personAfterModification2)
    }
}

希望对您有所帮助!

为 Person 创建一个扩展函数:

fun Person.modify(pm: PersonModification) {
    pm.firstName?.let { firstName = it }
    pm.lastName?.let { lastName = it }
    pm.job?.let { job = it }
}

fun Person.println() {
    println("firstName=$firstName, lastName=$lastName, job=$job")
}

并像这样使用它:

fun main(args: Array <String> ) {
    val p = Person("Nick", "Doe", "Cartoonist")
    print("Person before: ")
    p.println()

    val pm = PersonModification("Maria", null, "Actress")
    p.modify(pm)

    print("Person after: ")
    p.println()
}

或选择以下其中一项:

fun Person.println() {
    println("firstName=$firstName, lastName=$lastName, job=$job")
}

fun main(args: Array <String> ) {
    val p = Person("Nick", "Doe", "Cartoonist")
    print("Person before: ")
    p.println()

    val pm = PersonModification("John", null, null)

    pm.firstName?.run { p.firstName = this }.also { pm.lastName?.run { p.lastName = this } }.also { pm.job?.run { p.job = this } }
    // or
    pm.firstName?.also { p.firstName = it }.also { pm.lastName?.also { p.lastName = it } }.also { pm.job?.also { p.job = it } }
    // or 
    with (pm) {
        firstName?.run { p.firstName = this }
        lastName?.run { p.lastName= this }
        job?.run { p.job= this }
    }

    print("Person after: ")
    p.println()
}

模型映射实用程序

您还可以使用众多模型映射实用程序中的一种,例如 http://www.baeldung.com/java-performance-mapping-frameworks 中列出的实用程序(至少您已经看到了一些关于不同类型模型映射器的性能基准)。

请注意,如果您没有彻底测试,我真的不建议您编写自己的映射实用程序。已经看到自定义映射实用程序不断增长并随后导致奇怪行为的示例,因为未考虑某些极端情况。

简化 != null

否则,如果你不是太懒的话,我宁愿推荐这样的东西:

personModification.firstName?.also { person.firstName = it }

它不需要任何反射,简单且仍然可读......至少在某种程度上;-)

委托属性

我想到的另一件与您的 Javascript 方法相匹配的事情是 delegated properties(我仅在支持的 Map 是适合您的模型时才推荐它;实际上我在下面展示的是一个使用 HashMap 的委托人映射,我不能真正推荐它,但这是获得 Javascript 外观和感觉的一种非常简单和有用的方法;我不推荐它的原因: PersonMap 吗?;-)).

class Person() : MutableMap<String, String?> by HashMap() { // alternatively use class Person(val personProps : MutableMap<String, String?> = HashMap()) instead and replace `this` below with personProps
  var firstName by this
  var lastName by this
  var job by this
  constructor(firstName : String?, lastName : String?, job : String?) : this() {
    this.firstName = firstName
    this.lastName = lastName
    this.job = job
  }
}

然后PersonModification-class看起来基本一样。应用映射将如下所示:

val person = Person("first", "last", null)
val personMod = PersonModification("new first", null, "new job")
personMod.filterValues { it != null }
        .forEach { key, value -> person[key] = value } // here the benefit of extending the Map becomes visible: person[key] instead of person.personProps[key], but then again: person.personProps[key] is cleaner

如果您不需要辅助构造函数,那就更好了,那么 class 看起来几乎和以前一样,并且可以像以前一样设置和获取属性。

考虑一下,您实际上并不需要辅助构造函数,因为您仍然可以使用 apply,然后只需添加您感兴趣的变量(几乎作为命名参数)。然后 class 看起来类似于:

class PersonModification : MutableMap<String, String?> by HashMap() { // or again simply: class PersonModification(props : MutableMap<String, String?> = HashMap()) and replacing `this` with props below
  var firstName by this
  var lastName by this
  var job by this
}

然后实例化它如下所示:

val personMod = PersonModification().apply {
    firstName = "new first"
    job = "new job"
}

映射仍然是一样的。

这没什么特别的,但它向外界隐藏了变异 Person 的复杂性。

class Person(
        var firstName: String?,
        var lastName: String?,
        var job: String?
) {
    fun modify(p: PersonModification){
        p.firstName?.let { firstName = it }
        p.lastName?.let { lastName = it }
        p.job?.let { job = it }
    }
}

class PersonModification(/* ... */)