Kotlin - Перезаписывать Obj-стойки с модифицированными объективами Obj, если не Null - программирование
Подтвердить что ты не робот

Kotlin - Перезаписывать Obj-стойки с модифицированными объективами Obj, если не 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}

Длинная версия: у меня простая проблема. У меня есть класс 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, если свойство 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, лучше иметь избыточность, чем здесь отражать. Каковы мои другие варианты устранения избыточности?


Refelection:

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/info/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/

4b9b3361

Ответ 1

Это должно быть довольно просто написать 5-строчный помощник, чтобы сделать это, что даже поддерживает копирование каждого свойства соответствия или просто выбор свойств.

Хотя это, вероятно, не полезно, если вы пишете код Котлина и сильно используете классы данных и 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)
      }
    }
  }
}

Этот подход использует отражение, но он использует отражение Котлина, которое очень легкое. Я ничего не приурочил, но он должен работать почти с той же скоростью, что и копирование свойств вручную.

Кроме того, он использует KProperty вместо строк для определения подмножества свойств (если вы не хотите, чтобы все они были скопированы), поэтому он имеет полную поддержку рефакторинга, поэтому, если вы переименуете свойство в классе, вам не придется искать строку ссылки на переименование.

Он по умолчанию пропускает нули или вы можете переключить параметры skipNulls в false (по умолчанию это правда).

Теперь дано 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=)

Ответ 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}")


}

Ответ 3

Вы можете создать хороший признак для этого, который вы сможете применить для любого класса модификации, который у вас может быть:

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)

Ответ 4

Утилиты отображения модели

Вы также можете использовать одну из многих утилит отображения модели, например, те, которые перечислены в http://www.baeldung.com/java-performance-mapping-frameworks (там, по крайней мере, вы уже видите некоторые тесты производительности, относящиеся к разным типам модели картографы).

Обратите внимание, что я не могу рекомендовать писать собственную утилиту отображения, если вы ее не проверите полностью. Уже видели примеры, когда пользовательская утилита сопоставления росла и росла, а затем приводила к странному поведению, поскольку некоторые угловые случаи не рассматривались.

упрощение != null

В противном случае, если вы не слишком ленивы, я бы рекомендовал что-то вроде:

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

Это не требует никакого отражения, прост и по-прежнему читабель... как-то по крайней мере ;-)

делегированные свойства

Еще одна вещь, которая приходит мне на ум и каким-то образом соответствует вашему подходу Javascript, - это делегированные свойства (которые я рекомендую только в том случае, если поддерживаемая Map подходит для вас, на самом деле то, что я показываю ниже, является скорее делегированной картой человека с использованием HashMap, которую я не может по-настоящему рекомендовать, но это довольно простой и полезный способ получить внешний вид Javascript, поэтому я не рекомендую его: Person a 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 -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

Если вы не требуете этого вторичного конструктора, это даже лучше, тогда класс выглядит почти как раньше, и свойства могут быть установлены и как и раньше.

Подумав об этом, вам не нужен вторичный конструктор, как вы все еще можете использовать 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
}

и его экземпляр выглядит следующим образом:

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

Отображение по-прежнему будет одинаковым.

Ответ 5

Уже многие люди предлагали свои решения. Но я хочу предложить еще одно:

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

Надеюсь, что это поможет вам!

Ответ 6

Создайте функцию расширения для 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()
}

Ответ 7

Это ничего необычного, но это скрывает сложность мутирующего 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(/* ... */)