I have a data class like this:
data class Calculation(
val calculationType: String = "Multiplication", // or "Division"
val firstOperand: Int,
val secondOperand: Int,
val actualResult: Int = if (calculationType == "Multiplication") {
firstOperand * secondOperand
} else {
firstOperand / secondOperand
}
)
I noticed that there is a tight coupling between the different members of this data class
.
I wondered whether this can lead to problematic behavior or might defeat the purpose of data classes? Would this level of complexity already mean I should use a regular class
instead?
There is no issue providing default values for a data class, even when they are derived from other parameters. After all, it is just a default value and can be set to anything else when the object is constructed, and the final value may be completely independent of the other parameters.
From the semantics of your specific example, however, it looks like it shouldn't be possible to set actualResult
to anything else than the default value. In that case I would declare the class like this instead:
data class Calculation(
val calculationType: String = "Multiplication", // or "Division"
val firstOperand: Int,
val secondOperand: Int,
) {
val actualResult: Int = if (calculationType == "Multiplication") {
firstOperand * secondOperand
} else {
firstOperand / secondOperand
}
}
This is different in that
actualResult
cannot be set anymore, it is an entirely derived property that is calculated when the object is created.actualResult
is no longer taken into account when the additional data class functions (equals()
, hashCode()
, toString()
, component*(),
copy()
) are created by the compiler1. After all, the identity of an instance doesn't depend on actualResult
anymore.Use it only when the other parameters the calculation is based on are val
, not var
.
If you don't want to waste memory by storing derived data you can always calculate it on the fly instead:
data class Calculation(
val calculationType: String = "Multiplication", // or "Division"
val firstOperand: Int,
val secondOperand: Int,
) {
val actualResult: Int
get() = if (calculationType == "Multiplication") {
firstOperand * secondOperand
} else {
firstOperand / secondOperand
}
}
Now the property isn't calculated anymore when the object is created, it is calculated everytime it is accessed. The result wil be the same (since the other properties are val
and cannot change over time), but instead of consuming more memory it now slightly increases the load on the CPU when it is accessed multiple times.
Don't use this when the calculation is expensive. When the other properties are var
you may consider refactoring actualResult
to be a full function instead so its dynamic behavior becomes more clear to the caller.
And lastly, you can even change it to be a property that is lazily calculated:
data class Calculation(
val calculationType: String = "Multiplication", // or "Division"
val firstOperand: Int,
val secondOperand: Int,
) {
val actualResult: Int by lazy {
if (calculationType == "Multiplication") {
firstOperand * secondOperand
} else {
firstOperand / secondOperand
}
}
}
As in the previous example actualResult
is only calculated when it is accessed, but the result from the first calculation is stored so any subsequent access uses the stored result instead of repeating the calculation.
Use this only when
val
and not var
and1 The compiler only creates these functions for properties declared in the primary constructor, not in the body of the class: https://kotlinlang.org/docs/data-classes.html