Reputation: 53
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
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
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