kotlindata-class

Is it problematic to derive a default value of a member from other members in a data class?


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?


Solution

  • 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

    1. actualResult cannot be set anymore, it is an entirely derived property that is calculated when the object is created.
    2. 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


    1 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