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

时间:2018-07-06 14:28:40

标签: optimization kotlin dry

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}

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

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

我有一个叫PersonModification的班级:

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

如果Person属性不是PersonModification,则任务是用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.
//https://stackoverflow.com/questions/35525122/kotlin-data-class-how-to-read-the-value-of-property-if-i-dont-know-its-name-at
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/

7 个答案:

答案 0 :(得分:7)

编写一个5行的助手来做到这一点应该非常简单,该助手甚至支持复制每个匹配的属性或仅复制某些属性。

尽管如果您正在编写Kotlin代码并大量利用数据类和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而不是字符串来定义属性的子集(如果您不希望复制所有属性),因此它具有完整的重构支持,因此,如果在类上重命名属性,您将获得不必寻找要重命名的字符串引用。

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

现在提供2个课程:

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=)

答案 1 :(得分:2)

使用委托的属性可以解决此问题,而无需进行反思。参见: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}")


}

答案 2 :(得分:2)

您可以为此创建一个不错的 trait ,您将可以申请可能具有的任何修改类:

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)

答案 3 :(得分:2)

模型映射实用程序

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

请注意,如果您没有彻底测试它,我真的不建议您编写自己的映射实用程序。自定义映射实用程序不断增长的例子已经出现,后来由于不考虑某些极端情况而导致奇怪的行为。

简化!= null

否则,如果您不太懒惰,我宁愿推荐类似的东西:

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

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

委派的属性

我想到的另一件事是delegated properties(以某种方式与您的Javascript方法匹配)(仅当支持的Map是适合您的模型时,我才建议这样做;实际上,我在下面显示的是我无法真正推荐使用HashMap的委托人地图,但这是获得Javascript外观的一种非常简单而有用的方法;我不推荐它的原因是:Person a {{1 }}?;-))。

Map

然后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

如果您不需要二级构造函数就更好了,则该类看起来几乎和以前一样,并且可以像以前一样设置和获取属性。

考虑一下,您实际上并不需要辅助构造函数,因为您仍然可以使用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 ,然后只需添加您感兴趣的变量(几乎称为命名参数)即可。然后,该类将类似于:

apply

并将其实例化如下:

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
}

映射仍然相同。

答案 4 :(得分:0)

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

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)
    }
}

希望这对您有帮助!

答案 5 :(得分:0)

为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()
}

答案 6 :(得分:0)

这没什么花哨的,但它掩盖了从外部世界对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(/* ... */)