Home > Net >  Object copying, Kotlin
Object copying, Kotlin

Time:08-12

I'm trying to model car object in Kotlin, and I have a following structure:

data class CarModel(
    val brand: Brand, val modelName: String
) {
    val versionNumber = VersionNumber(value = 1)


    override fun toString(): String {
        return "CarModel(brand=$brand, modelName=$modelName, versionNumber=${versionNumber.value})"
    }

    fun incrementVersionNumber(): CarModel {
        val newCarModel = this.copy()
        newCarModel.versionNumber = this.versionNumber.value   1 //ERROR (Val cannot be reassigned)

        return newCarModel
    }
}

data class VersionNumber(val value: Int) {}

Since version number always has to be 1 initially, the idea is to allow incrementation of the version number only through incrementVersionNumber method. I tried to achieve that with object cloning, meaning return the copy of the object only with version number changed. But for that to work i would need to change the versionNumber property to var but i want to keep it immutable. Is there some way to get this to work while keeping the versionNumber property as val

CodePudding user response:

You could do what you ask with a private constructor (including the version number), a factory method to create new instances and the copy method to create instances with versions != 1.

data class CarModel private constructor(
    val brand: Brand, val modelName: String, val versionNumber: VersionNumber
) {
    companion object {
        fun create(brand: Brand, modelName: String): CarModel {
             return CarModel(brand, modelName, VersionNumber(1))
        }
    }
    fun incrementVersionNumber(): CarModel =
        this.copy(versionNumber = VersionNumber(versionNumber.value   1))
}

Also you are probably easier off using an inline class for VersionNumber instead.

CodePudding user response:

If you want to completely lock down unexpected behaviour; for example, someone deliberately changing versionNumber to 0, then you would need to use a regular class rather than a data class, as you'll be able to return new instances with any of the properties changed using copy().

Consider this approach, which uses a secondary constructor to prevent erroneous entries on instantiation.

class CarModel private constructor (val brand: Brand, val modelName: String, val versionNumber: VersionNumber) {
    
    constructor(brand: Brand, modelName: String) : this(brand, modelName, VersionNumber())
    
    fun incrementVersionNumber(): CarModel {
        return CarModel(brand, modelName, VersionNumber(versionNumber.value   1))
    }
    
    override fun toString(): String {
        return "CarModel(brand=$brand, modelName=$modelName, versionNumber=${versionNumber.value})"
    }
}

data class VersionNumber(val value: Int = 1)

You could also introduce an init block to check that version is greater than zero, but you won't be able to check that the current version number is an increment of the previous instance:

data class CarModel(val brand: Brand, val modelName: String, val versionNumber: VersionNumber = VersionNumber()) {

    init {
        require(versionNumber.value > 0) { "Version number must be greater than zero." }
    }
    
    fun incrementVersionNumber(): CarModel {
        return CarModel(brand, modelName, VersionNumber(versionNumber.value   1))
    }
    
    override fun toString(): String {
        return "CarModel(brand=$brand, modelName=$modelName, versionNumber=${versionNumber.value})"
    }
}

You could also move the require check to the VersionNumber class:

data class VersionNumber(val value: Int = 1) {
    init {
        require(value > 0) { "Version number must be greater than zero." }
    }
}

Finally, if you really want a class to behave like a data class, then you will need to override the default implementations for equals and hashCode in order to obtain proper value semantics:

class CarModel(val brand: Brand, val modelName: String, val versionNumber: VersionNumber = VersionNumber()) {
    
    fun incrementVersionNumber(): CarModel {
        return CarModel(brand, modelName, VersionNumber(versionNumber.value   1))
    }
    
    override fun equals(other: Any?): Boolean {
        return this === other
                || other is CarModel
                && other.brand == brand
                && other.modelName == modelName
                && other.versionNumber == versionNumber
    }
    
    override fun hashCode(): Int {
        return Objects.hash(brand, modelName, versionNumber)
    }
    
    override fun toString(): String {
        return "CarModel(brand=$brand, modelName=$modelName, versionNumber=${versionNumber.value})"
    }
}

CodePudding user response:

You can use a private setter in your CarModel class:

var versionNumber: VersionNumber = VersionNumber(value = 1)
    private set

fun incrementVersionNumber(): CarModel = copy().apply {
    versionNumber = VersionNumber(versionNumber.value   1)
}

Also, you are probably easier off using an inline class for VersionNumber instead.

  • Related