markus-wa
markus-wa

Reputation: 53

Re-use mapping code for immutable data class in Kotlin

Updated: added some clarifications from the comments

I would like to use the same 'mapping' code for the primary constructor and copy() method of an immutable data class. How can I do this without creating an empty object first, and then using copy() on it?

The issue with how it is now is that if I add a new attribute with default value to Employee and EmployeeForm it would be easy to only add it in one of the two mapping functions and forget about the other (toEmployeeNotReusable / copyEmployee).

These are the data classes I'd like to map between:

@Entity
data class Employee(
    val firstName: String,
    val lastName: String,
    val jobType: Int,

    @OneToMany(mappedBy = "employee", cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
    private val _absences: MutableSet<Absence> = mutableSetOf(),

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long = 0 // prevents @Joffrey's answer from working
) {
    init {
        _absences.forEach { it.employee = this }
    }

    val absences get() = _absences.toSet()

    fun addAbsence(newAbsence: Absence) {
        newAbsence.employee = this
        _absences += newAbsence
    }

    @Entity
    @Table(name = "absence")
    data class Absence(
        // ... omitted fields
    ) {
        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "employee_id")
        lateinit var employee: Employee
    }
}


data class EmployeeForm(
    var firstName: String = "",
    var lastName: String = "",
    var jobType: Int = 0
) {
    // not reusable
    fun toEmployeeNotReusable(): Employee {
        return Employee(firstName, lastName, jobType)
    }

    // works but hacky
    fun toEmployee(): Employee {
        return copyEmployee(Employee("", "", 0))
    }

    fun copyEmployee(employee: Employee): Employee {
        return employee.copy(
            firstName = firstName,
            lastName = lastName,
            jobType = jobType
        )
    }
}

While mutability would be fine, in my case, I'd be interested to know how this would be possible.

Upvotes: 0

Views: 570

Answers (2)

Alexey Romanov
Alexey Romanov

Reputation: 170805

You should be able to do this using reflection: check list of properties in Employee and EmployeeForm, call the constructor by the matching names (using callBy to handle default parameters). The drawback, of course, is that you won't get compile-time errors if any properties are missing (but for this case, any test would probably fail and tell you about the problem).

Approximate and untested (don't forget to add the kotlin-reflect dependency):

inline fun <reified T> copy(x: Any): T {
    val construct = T::class.primaryConstructor
    val props = x::class.memberProperties.associate { 
        // assumes all properties on x are valid params for the constructor
        Pair(construct.findParameterByName(it.name)!!,
             it.call(x))
    }
    return construct.callBy(props)
}

// in EmployeeForm
fun toEmployee() = copy<Employee>(this)

You can make an equivalent which is compile-time checked with Scala macros, but I don't think it's possible in Kotlin.

Upvotes: 1

Joffrey
Joffrey

Reputation: 37729

One way to avoid listing the attributes 4 times would be to declare Employee as an interface instead, and use the "mutable" version, the form, as the only data class implementing it. You would have the "read-only" view using the interface, but you would technically only use the mutable instance behind the scenes.

This would follow what Kotlin designers have done for List vs MutableList.

interface Employee {
    val firstName: String
    val lastName: String
    val jobType: Int
}

data class EmployeeForm(
    override var firstName: String = "",
    override var lastName: String = "",
    override var jobType: Int = 0
): Employee {

    fun toEmployee(): Employee = this.copy()

    fun copyEmployee(employee: Employee): Employee = this.copy(
            firstName = firstName,
            lastName = lastName,
            jobType = jobType
    )
}

However, this implies that the form has all fields of an employee, which you probably don't want.

Also, I would personally prefer what you had done in the beginning, listing twice the field would not be a problem, just write tests for your functions, and when you want to add functionality, you'll add tests for that functionality anyway.

Upvotes: 1

Related Questions