The init{} block has its flaws. It cannot guarantee the null safety of abstract fields. It also cannot guarantee that the value of an open field is what its final value will be, as it could get overwritten multiple times.
I propose the instance{} block. This block will run after the object is fully instantiated. This will guarantee that the abstract fields in the Object are not null, and that the open fields have the latest overwritten value.
Given: a Var is a class which resets its value when another Value<*> which it depends on resets its value. Calling getValue() lazily computes the value if it has been reset.
abstract class BoundingBox {
abstract val x: Var<Float>
abstract val y: Var<Float>
open val width: Var<Float> = Var { 0f }
open val height: Var<Float> = Var { 0f }
val bounds: Var<Rectangle> = Var {
Rectangle(
x.getValue(),
y.getValue(),
width.getValue(),
height.getValue()
)
}
init {
bounds.dependsOn(x, y, width, height)
}
}
BoundingBox wants to define that whatever x, y, width, height values eventually get assigned to it will trigger the reset of the bounds Var. This is impossible using init{} as it will throw NPE for x and y, and will operate on the default non-overridden value of width and height.
To solve this, you could require the abstract fields to be passed in as params, but if you have many fields like this, it would become cluttered. Open fields need more confusing logic. The whole thing very user-unfriendly.
Another solution would be to define a function onCreate() which could have the logic contained by init{} block in above example. Problem here is that the user must implement this each time, and there’s no guarantee they will. Or they may call it multiple times, or not immediately after creation, creating an object state that cannot be guaranteed.
Having an instance{} block whose logic can be defined in the original abstract object and then implemented by the final extension of that object solves these problems in a clean, reliable, and enforceable way.