Issue
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
Solution
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})"
}
}
Answered By - Matthew Layton
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.