Is there a way to merge kotlin data classes without specifying all the properties?
data class MyDataClass(val prop1: String, val prop2: Int, ...//many props)
with a function with the following signature:
fun merge(left: MyDataClass, right: MyDataClass): MyDataClass
where this function checks each property on both classes and where they are different uses the left parameter to create a new MyDataClass.
Is this possible possible using kotlin-reflect, or some other means?
EDIT: more clarity
Here is a better description of what i want to be able to do
data class Bob(
val name: String?,
val age: Int?,
val remoteId: String?,
val id: String)
#Test
fun bob(){
val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")
//TODO: merge without accessing all properties
// val result =
assertThat(result).isEqualTo(Bob(id = "local_id", name = "Ben", age=1, remoteId = "remote_id"))
}
If you want to copy values from the right when values in the left are null then you can do the following:
inline infix fun <reified T : Any> T.merge(other: T): T {
val propertiesByName = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor
?: throw IllegalArgumentException("merge type must have a primary constructor")
val args = primaryConstructor.parameters.associateWith { parameter ->
val property = propertiesByName[parameter.name]
?: throw IllegalStateException("no declared member property found with name '${parameter.name}'")
(property.get(this) ?: property.get(other))
}
return primaryConstructor.callBy(args)
}
Usage:
data class MyDataClass(val prop1: String?, val prop2: Int?)
val a = MyDataClass(null, 1)
val b = MyDataClass("b", 2)
val c = a merge b // MyDataClass(prop1=b, prop2=1)
A class-specific way to combine data classes when we can define the fields we want to combine would be:
data class SomeData(val dataA: Int?, val dataB: String?, val dataC: Boolean?) {
fun combine(newData: SomeData): SomeData {
//Let values of new data replace corresponding values of this instance, otherwise fall back on the current values.
return this.copy(dataA = newData.dataA ?: dataA,
dataB = newData.dataB ?: dataB,
dataC = newData.dataC ?: dataC)
}
}
#mfulton26's solution merges properties that are part of primary constructor only. I have extended that to support all properties
inline infix fun <reified T : Any> T.merge(other: T): T {
val nameToProperty = T::class.declaredMemberProperties.associateBy { it.name }
val primaryConstructor = T::class.primaryConstructor!!
val args = primaryConstructor.parameters.associate { parameter ->
val property = nameToProperty[parameter.name]!!
parameter to (property.get(other) ?: property.get(this))
}
val mergedObject = primaryConstructor.callBy(args)
nameToProperty.values.forEach { it ->
run {
val property = it as KMutableProperty<*>
val value = property.javaGetter!!.invoke(other) ?: property.javaGetter!!.invoke(this)
property.javaSetter!!.invoke(mergedObject, value)
}
}
return mergedObject
}
Your requirements are exactly the same as copying the left value:
fun merge(left: MyDataClass, right: MyDataClass) = left.copy()
Perhaps one of use isn't properly understanding the other. Please elaborate if this isn't what you want.
Note that since right isn't used, you could make it a vararg and "merge" as many as you like :)
fun merge(left: MyDataClass, vararg right: MyDataClass) = left.copy()
val totallyNewData = merge(data1, data2, data3, data4, ...)
EDIT
Classes in Kotlin don't keep track of their deltas. Think of what you get as you're going through this process. After the first change you have
current = Bob("Ben", null, null, "local_id")
next = Bob(null, 1, null, "local_id")
How is it supposed to know that you want next to apply the change to age but not name? If you're just updating based on nullability,
#mfulton has a good answer. Otherwise you need to provide the information yourself.
infix fun <T : Any> T.merge(mapping: KProperty1<T, *>.() -> Any?): T {
//data class always has primary constructor ---v
val constructor = this::class.primaryConstructor!!
//calculate the property order
val order = constructor.parameters.mapIndexed { index, it -> it.name to index }
.associate { it };
// merge properties
#Suppress("UNCHECKED_CAST")
val merged = (this::class as KClass<T>).declaredMemberProperties
.sortedWith(compareBy{ order[it.name]})
.map { it.mapping() }
.toTypedArray()
return constructor.call(*merged);
}
Edit
infix fun <T : Any> T.merge(right: T): T {
val left = this;
return left merge mapping# {
// v--- implement your own merge strategy
return#mapping this.get(left) ?: this.get(right);
};
}
Example
val original = Bob(id = "local_id", name = null, age = null, remoteId = null)
val withName = original.copy(name = "Ben")
val withAge = original.copy(age = 1)
val withRemoteId = original.copy(remoteId = "remote_id")
val result = withName merge withAge merge withRemoteId;
Related
I am creating a data class in kotlin as such
data class User(val name: String, val age: Int)
{
constructor(name: String, age: Int, size: String): this(name, age) {
}
}
In my main function, I can access the objects as such:
fun main(){
val x = User("foo", 5, "M")
println(x.name)
println(x.age)
println(x.size) // does not work
}
My problem is that I can't get access to size.
What I am trying to do is, create a data class where top level params are the common items that will be accessed, and in the constructors, have additional params that fit certain situations. The purpose is so that I can do something like
// something along the lines of
if (!haveSize()){
val person = User("foo", 5, "M")
} else {
val person = User("foo", 5)
}
}
Any ideas?
In Kotlin you do not need separate constructors for defining optional constructor params. You can define them all in a single constructor with default values or make them nullable, like this:
data class User(val name: String, val age: Int, val size: String = "M")
fun main(){
val x = User("foo", 5, "L")
val y = User("foo", 5)
println(x.size) // "L" from call site
println(y.size) // "M" from default param
}
You can not access size variable, because this is from secondary construct, but we have alternative variant.
data class User(var name: String, var age: Int) {
var size: String
init {
size = "size"
}
constructor(name: String, age: Int, size: String) : this(name, age) {
this.size = size
}
}
In short, you want to have one property that can be one of a limited number of options. This could be solved using generics, or sealed inheritance.
Generics
Here I've added an interface, MountDetails, with a generic parameter, T. There's a single property, val c, which is of type T.
data class User(
val mountOptions: MountOptions,
val mountDetails: MountDetails<*>,
)
data class MountOptions(
val a: String,
val b: String
)
interface MountDetails<T : Any> {
val c: T
}
data class MountOneDetails(override val c: Int) : MountDetails<Int>
data class MountTwoDetails(override val c: String) : MountDetails<String>
Because the implementations MountDetails (MountOneDetails and MountTwoDetails) specify the type of T to be Int or String, val c can always be accessed.
fun anotherCaller(user: User) {
println(user.mountOptions.a)
println(user.mountOptions.b)
println(user.mountDetails)
}
fun main() {
val mt = MountOptions("foo", "bar")
val mountOneDetails = MountOneDetails(111)
anotherCaller(User(mt, mountOneDetails))
val mountTwoDetails = MountTwoDetails("mount two")
anotherCaller(User(mt, mountTwoDetails))
}
Output:
foo
bar
MountOneDetails(c=111)
foo
bar
MountTwoDetails(c=mount two)
Generics have downsides though. If there are lots of generic parameters it's messy, and it can be difficult at runtime to determine the type of classes thanks to type-erasure.
Sealed inheritance
Since you only have a limited number of mount details, a much neater solution is sealed classes and interfaces.
data class User(val mountOptions: MountOptions)
sealed interface MountOptions {
val a: String
val b: String
}
data class MountOneOptions(
override val a: String,
override val b: String,
val integerData: Int,
) : MountOptions
data class MountTwoOptions(
override val a: String,
override val b: String,
val stringData: String,
) : MountOptions
The benefit here is that there's fewer classes, and the typings are more specific. It's also easy to add or remove an additional mount details, and any exhaustive when statements will cause a compiler error.
fun anotherCaller(user: User) {
println(user.mountOptions.a)
println(user.mountOptions.b)
// use an exhaustive when to determine the actual type
when (user.mountOptions) {
is MountOneOptions -> println(user.mountOptions.integerData)
is MountTwoOptions -> println(user.mountOptions.stringData)
// no need for an 'else' branch
}
}
fun main() {
val mountOne = MountOneOptions("foo", "bar", 111)
anotherCaller(User(mountOne))
val mountTwo = MountTwoOptions("foo", "bar", "mount two")
anotherCaller(User(mountTwo))
}
Output:
foo
bar
111
foo
bar
mount two
This is really the "default values" answer provided by Hubert Grzeskowiak adjusted to your example:
data class OneDetails(val c: Int)
data class TwoDetails(val c: String)
data class MountOptions(val a: String, val b: String)
data class User(
val mountOptions: MountOptions,
val detailsOne: OneDetails? = null,
val detailsTwo: TwoDetails? = null
)
fun main() {
fun anotherCaller(user: User) = println(user)
val mt = MountOptions("foo", "bar")
val one = OneDetails(1)
val two = TwoDetails("2")
val switch = "0"
when (switch) {
"0" -> anotherCaller(User(mt))
"1" -> anotherCaller(User(mt, detailsOne = one))
"2" -> anotherCaller(User(mt, detailsTwo = two))
"12" -> anotherCaller(User(mt, detailsOne = one, detailsTwo = two))
else -> throw IllegalArgumentException(switch)
}
}
I have the following dataclasses:
data class JsonNpc(
val name: String,
val neighbours: JsonPreferences
)
data class JsonPreferences(
val loves: List<String>,
val hates: List<String>
)
I have a list of these, and they reference each other through strings like:
[
JsonNpc(
"first",
JsonPreferences(
listOf("second"),
listOf()
)
),
JsonNpc(
"second",
JsonPreferences(
listOf(),
listOf("first")
)
)
]
note that a likes b does not mean b likes a
I also have the Dataclasses
data class Npc(
val name: String,
val neighbours: NeighbourPreferences,
)
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
And I want to convert the String reference types to the normal reference types.
What I have tried:
recursively creating the npcs (and excluding any that are already in the chain, as that would lead to infinite recursion):
Does not work, as the Npc can not be fully created and the List is immutable (I dont want it to be mutable)
I have managed to find a way to do this. It did not work with Npc as a data class, as I needed a real constructor
fun parseNpcs(map: Map<String, JsonNpc>): Map<String, Npc> {
val resultMap: MutableMap<String, Npc> = mutableMapOf()
for (value in map.values) {
if(resultMap.containsKey(value.name))
continue
Npc(value, map, resultMap)
}
return resultMap
}
class Npc(jsonNpc: JsonNpc, infoList: Map<String, JsonNpc>, resultMap: MutableMap<String, Npc>) {
val name: String
val neighbourPreferences: NeighbourPreferences
init {
this.name = jsonNpc.name
resultMap[name] = this
val lovesNpc = jsonNpc.neighbours.loves.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
val hatesNpc = jsonNpc.neighbours.hates.map {
resultMap[it] ?: Npc(infoList[it] ?: error("Missing an Npc"), infoList, resultMap)
}
this.neighbourPreferences = NeighbourPreferences(
lovesNpc, hatesNpc
)
}
}
data class NeighbourPreferences(
val loves: List<Npc>,
val hates: List<Npc>
)
checking in the debugger, the people carry the same references for each Neighbour, so the Guide is always one Npc instance.
To start I have the following Moshi json.
#JsonClass(generateAdapter = true)
data class OrderDetails(
#Json(name = "_id") val id: Int,
#Json(name = "status") val status: String,
#Json(name = "tableNo") val tableNo: Int,
#Json(name = "serverId") val serverId: Int?,
#Json(name = "items") val orderItems: List<OrderDetailsItem>
)
All these fields are expected to have data except for serverId.
This data is fetched from the server where I can allow the user to select order.
onSeletedOrder
.map { it.orderDetails.serverId } //blows up here apparently.
.filterNotNull() //have tried this but it doesn't matter.
.flatMap { findServerBy(it) }
.map { "${it.firstname} ${it.lastname}" }
When I map to the serverId above I blow up with an NPE.
It's interesting that the map (even though it is optional) does an unsafe cast afterwards.
I'd expect it to maintain the optional-ness after the map.
I'm assuming this is because of the bridging backwards to RxJava.
Curious if anyone has a further explanation on why this is.
RxJava does not allow nulls inside the stream. Ideally you would perform this filter before the items enter the stream, but if you can't do that one workaround you could get away with is to use an empty string in place of null.
onSeletedOrder
.map { it.orderDetails.serverId.orEmpty() }
.filter { it.isNotEmpty() }
.flatMap { findServerBy(it) }
.map { "${it.firstname} ${it.lastname}" }
For "map, but exclude some elements", RxJava has flatMapMaybe (other types than Observable can also have it with corresponding return type):
// helper
fun <T> T?.toMaybe(): Maybe<T> = if (this != null) Maybe.just(this) else Maybe.empty<T>
onSeletedOrder
.flatMapMaybe { it.orderDetails.serverId.toMaybe() }
.flatMap { findServerBy(it) }
.map { "${it.firstname} ${it.lastname}" }
The best approach is to format your Data Class with optional data.
#JsonClass(generateAdapter = true)
data class OrderDetails(
#Json(name = "_id") val id: Int? = 0,
#Json(name = "status") val status: String? = "Not active",
#Json(name = "tableNo") val tableNo: Int? = 0,
#Json(name = "serverId") val serverId: Int? = 0,
#Json(name = "items") val orderItems: List<OrderDetailsItem>? = listOf()
)
You won't receive any NPE
Given an update request for a record in DB, I have to find a difference between the payload and existing data in DB then create a new Object which has updated fields with Payload values and rest as Null.
I have created a function which gives me a list of field names which were updated, But I'm unable to create a new object which has values for only these updated fields.The problem is that the function uses "field: Field in cpayload.javaClass.declaredFields" which is kind of generic so I'm unable to set these fields.
fun findupdatedFieldsList(cpayload: Customer, cEntity: Customer): List<String> {
// var customerToPublish = Customer()
val updatedFieldsList: MutableList<String>
updatedFieldsList = ArrayList()
for (field: Field in cpayload.javaClass.declaredFields) {
field.isAccessible = true
val value1 = field.get(cpayload).toString()
val value2 = field.get(cEntity).toString()
!Objects.equals(value1, value2).apply {
if (this) {
// customerToPublish.birthDate=field.get(cpayload).toString()
updatedFieldsList.add(field.name)
}
}
}
return updatedFieldsList
}
#Entity
#Table
data class Customer(
#Id
val partyKey: UUID,
var preferredName: String?,
var givenName: String?,
var lastName: String?,
var middleName: String?,
var emailAddress: String,
var mobileNumber: String,
val birthDate: String?,
val loginOnRegister: Boolean,
var gender: Gender?,
var placeOfBirth: String?,
var createdDate: LocalDateTime = LocalDateTime.now(),
var updatedDate: LocalDateTime = LocalDateTime.now()
)
Desired Output
val customer = Customer(
preferredName = Updated name,
partyKey = partyKey.value,
givenName = Updated name,
lastName = null,
middleName = null,
emailAddress = Updated email,
mobileNumber = null,
birthDate = null,
gender = null,
placeOfBirth = null
)
I was able to construct a solution using Kotlin's reflect. It is generic and can be applied to any Kotlin class that have primary constructor. Unfortunately it won't work with Java classes
You would need to add kotlin-reflect package to your build tool config, e.g. for Gradle:
implementation 'org.jetbrains.kotlin:kotlin-reflect:XXXXXX'
First we will build a function to extract updated properties. Please take a note that we also need to extract properties that are mandatory (non-nullable and without default). We add them to a map of propertyName -> propertyValue:
fun Map<String?, KParameter>.isOptional(name: String) = this[name]?.isOptional ?: false
fun <T : Any> findUpdatedProperties(payload: T, entity: T): Map<String, Any?> {
val ctorParams = payload::class.primaryConstructor!!.parameters.associateBy { it.name }
return payload::class.memberProperties.map { property ->
val payloadValue = property.call(payload)
val entityValue = property.call(entity)
if (!Objects.equals(payloadValue, entityValue) || (!ctorParams.isOptional(property.name))) {
property.name to payloadValue
} else {
null
}
}
.filterNotNull()
.toMap()
}
Then we call this function and construct a new instance of provided class:
fun <T : Any> constructCustomerDiff(clazz: KClass<T>, payload: T, entity: T): T {
val ctor = clazz.primaryConstructor!!
val params = ctor.parameters
val updatedProperties = findUpdatedProperties(payload, entity)
val values = params.map { it to updatedProperties[it.name] }.toMap()
return ctor.callBy(values)
}
Take a note that missing primary constructor will throw NullPointerException because of use of !!.
We could call this funcion as constructCustomerDiff(Customer::class, payload, entity), but we can do better with reified types:
inline fun <reified T : Any> constructCustomerDiff(payload: T, entity: T): T {
return constructCustomerDiff(T::class, payload, entity)
}
Now we can use this function in convenient Kotlin style:
val id = UUID.randomUUID()
val payload = Customer(
partyKey = id,
preferredName = "newName",
givenName = "givenName"
)
val entity = Customer(
partyKey = id,
preferredName = "oldName",
givenName = "givenName" // this is the same as in payload
)
val x = constructCustomerDiff(payload, entity)
assert(x.partyKey == id && x.givenName == null || x.preferredName == "newName")
TL;DR:
How do I make this less redundant (any approach that works helps)?
if (personModification.firstName != null) {person.firstName = personModification.firstName}
if (personModification.lastName != null) {person.lastName = personModification.lastName}
if (personModification.job != null) {person.job = personModification.job}
The long version: I have a simple problem. I have a class Person:
class Person (val firstName: String?,
val lastName: String?,
val job: String?)
and I have a class called PersonModification:
class PersonModification(val firstName: String?,
val lastName: String?,
val job: String?)
The task is to overwrite any Person property values with PersonModification values, IF the PersonModification property isn't null. If you care, the business logic behind this is an API endpoint which modifies Person and takes a PersonModification as an argument (but can change all, or any, of the properties, so we don't want to overwrite valid old values with nulls). The solution to this looks like this.
if (personModification.firstName != null) {person.firstName = personModification.firstName}
if (personModification.lastName != null) {person.lastName = personModification.lastName}
if (personModification.job != null) {person.job = personModification.job}
I was told this is redundant (and I agree). The solution pseudocode looks like this:
foreach(propName in personProps){
if (personModification["propName"] != null) {person["propName"] = personModification["propName"]}
}
Of course, this isn't JavaScript, so it's not that easy. My reflection solution is below, but imo, it's better to have redundancy than do reflection here. What are my other options to remove the redundancy?
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/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")
}
}
})
}
You can copy and paste here to run it: https://try.kotlinlang.org/
It should be pretty simple to write a 5 line helper to do this which even supports copying every matching property or just a selection of properties.
Although it's probably not useful if you're writing Kotlin code and heavily utilising data classes and val (immutable properties). Check it out:
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)
}
}
}
}
This approach uses reflection, but it uses Kotlin reflection which is very lightweight. I haven't timed anything, but it should run almost at same speed as copying properties by hand.
Also it uses KProperty instead of strings to define a subset of properties (if you don't want all of them copied) so it has complete refactoring support, so if you rename a property on the class you won't have to hunt for string references to rename.
It will skip nulls by default or you can toggle the skipNulls parameters to false (default is true).
Now given 2 classes:
data class DataOne(val propA: String, val propB: String)
data class DataTwo(var propA: String = "", var propB: String = "")
You can do the following:
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)
Output will be:
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=)
This can be solved without reflection using delegated properties. See: 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}")
}
You can create a nice trait for this which you will be able to apply for any modification class you might have:
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)
}
}
}
}
}
Usage:
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?)
Then you can try it out:
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)
}
This will print:
Person(firstName=John, lastName=null, job=null)
Person(firstName=John, lastName=Doe, job=null)
Person(firstName=John, lastName=Doe, job=Unemployed)
model mapping utilities
You can also use one of the many model mapping utilities, like the ones listed in http://www.baeldung.com/java-performance-mapping-frameworks (there at least you already see some performance benchmarks regarding the different kind of model mappers).
Note that I cannot really recommend writing your own mapping utility if you do not test it thoroughly. Already seen examples where the custom mapping utility grew and grew and later on lead to strange behaviour as some corner cases weren't considered.
simplifying the != null
Otherwise, if you are not too lazy, I would rather recommend something like:
personModification.firstName?.also { person.firstName = it }
It doesn't require any reflection, is simple and still readable... somehow at least ;-)
delegated properties
Another thing that comes to my mind and somehow matches your Javascript approach are delegated properties (which I only recommend if the backed Map is a suitable model for you; actually what I am showing below is rather a delegated person map using a HashMap, which I can not really recommend, but which is quite an easy and useful way to get the Javascript look&feel; the reason why I don't recommend it: is 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
}
}
The PersonModification-class then basically looks the same. Applying the mapping would then look like:
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
If you do not require that secondary constructor it's even better, then the class looks nearly as before and the properties can be set and get as before.
Thinking about it you do not really need the secondary constructor as you could still use apply and then just add the variables you are interested in (nearly as named parameters). Then the class would look similar to:
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
}
and instantiating it then looks as follows:
val personMod = PersonModification().apply {
firstName = "new first"
job = "new job"
}
Mapping would still be the same.
Already many people offered their solutions. But I want to offer one more:
There are interesting feature in jackson, you could try to merge json. So, you could merge src object with deserialization version of PersonModification
With it, it's possible to do something like this:
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)
}
}
Hope this will help you!
Create an extension function for 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")
}
and use it like this:
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()
}
Or choose one of the following:
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()
}
It is nothing fancy, but it hides the complexity of mutating Person from the outside world.
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(/* ... */)