I want use type-safe builders in Kotlin for a type that has immutable properties.
class DataClass(val p1:String, val p2:String) {}
fun builder(buildBlock: DataClass.() -> Unit) {
return DataClass(p1 = "",p2 = "").also(block)
}
//...
builder {
p1 = "p1" // <-- This won't compile, since p1 is a val
p2 = "p2" // <-- This won't compile, since p2 is a val
}
I thought of two solutions to work around this:
Option 1: Create a builder class:
class DataClass(val p1: String, val p2: String) {}
class DataClassBuilder(){
lateinit var p1: String
lateinit var p2: String
fun build() = DataClass(p1, p2)
}
fun builder(buildBlock: DataClassBuilder.() -> Unit) {
return DataClassBuilder().also(block).build()
}
Option 2: Create a custom delegate to prevent setting the value again:
class InitOnceDelegate: ReadWriteProperty<DTest, String> {
private var state: String? = null
override fun getValue(thisRef: DTest, property: KProperty<*>): String {
return state ?: throw IllegalStateException()
}
override fun setValue(thisRef: DTest, property: KProperty<*>, value: String) {
if (state == null) {
state = value
} else {
throw IllegalStateException("${property.name} has already been initialized")
}
}
}
class DataClass() {
var p1: String by InitOnceDelegate()
var p2: String by InitOnceDelegate()
}
fun builder(buildBlock: DataClass.() -> Unit) {
return DataClass(p1 = "",p2 = "").also(block)
}
//...
val d = builder {
p1 = "p1"
p2 = "p2"
}
d.p1 = "another value" // <-- This will throw an exception now.
Option 1 has the drawback that I have to maintain two class, option 2 has the drawback that the compiler will allow setting the value in DataClass
again and the check will only happen at runtime.
Is there a better way to solve this without the mentioned drawbacks?