Explain the internal design behind observable, vetoable, and notNull. How would you build a custom delegate that both validates and reacts?
answer
- ObservableProperty: beforeChange (gate) + afterChange (react)
- setValue: beforeChange -> write -> afterChange
- false from beforeChange = early return = veto
- notNull = separate NotNullVar, not ObservableProperty
- Subclass both hooks to validate AND react
basics
~20 sobservable and vetoable share one base class with two hooks: one decides whether to allow a change, the other runs after it. notNull is a separate delegate that throws if read before set. You can subclass the base to both check and react.
solid answer
~30 sobservable and vetoable are both backed by the abstract `ObservableProperty<T>` class, which holds the backing value and implements `setValue` to call two overridable hooks: `beforeChange(property, old, new): Boolean` (gate — default returns true) and `afterChange(property, old, new): Unit` (reaction — default no-op). `vetoable` overrides `beforeChange`; `observable` overrides `afterChange`. `notNull()` is unrelated to that hierarchy — it returns a `NotNullVar` that throws `IllegalStateException` on read before assignment. To validate *and* react in one delegate, subclass `ObservableProperty<T>` and override both hooks, or implement `ReadWriteProperty<R, T>` directly for full control over `getValue`/`setValue`. The hooks receive `KProperty<*>`, enabling generic, reusable delegates.
code
kotlin · 19 linesimport kotlin.properties.ObservableProperty
import kotlin.reflect.KProperty
class NonBlank(initial: String) : ObservableProperty<String>(initial) {
override fun beforeChange(p: KProperty<*>, old: String, new: String) =
new.isNotBlank() // veto blanks
override fun afterChange(p: KProperty<*>, old: String, new: String) {
println("${p.name} set to '$new'") // react
}
}
class Form { var title: String by NonBlank("untitled") }
fun main() {
val f = Form()
f.title = "Hello" // title set to 'Hello'
f.title = " " // vetoed, stays 'Hello'
println(f.title) // Hello
}go deeper
Can describe at a high level that there's a hook before and after the change.
Names beforeChange/afterChange and that vetoable/observable override one each.
Reproduces the setValue ordering, builds a custom ObservableProperty overriding both hooks, and knows the hooks can't transform values.
Discusses when to drop down to ReadWriteProperty/provideDelegate, reuse via KProperty, thread-safety of the shared backing field, and delegate overhead trade-offs.
## The ObservableProperty base Kotlin's stdlib defines an abstract class roughly like: ```kotlin abstract class ObservableProperty<V>(initialValue: V) : ReadWriteProperty<Any?, V> { private var value = initialValue protected open fun beforeChange(property: KProperty<*>, oldValue: V, newValue: V): Boolean = true protected open fun afterChange(property: KProperty<*>, oldValue: V, newValue: V): Unit {} override fun getValue(thisRef: Any?, property: KProperty<*>): V = value override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) { val oldValue = this.value if (!beforeChange(property, oldValue, value)) return // veto this.value = value afterChange(property, oldValue, value) } } ``` Key takeaways: - `setValue` calls **`beforeChange` first**. If it returns `false`, it `return`s early — the field is never updated. That is the veto mechanism. - If allowed, it writes the field, then calls **`afterChange`**. That is the reaction mechanism. - `Delegates.vetoable` supplies a `beforeChange` that delegates to your Boolean lambda; `Delegates.observable` supplies an `afterChange` that delegates to your Unit lambda. ## notNull is separate `Delegates.notNull()` returns a small `NotNullVar<T>` that stores a nullable backing field and throws `IllegalStateException("Property ${property.name} should be initialized before get.")` if read while still null. It does **not** extend `ObservableProperty`. ## Building a validate-and-react delegate The cleanest path is to subclass `ObservableProperty` and override both hooks: ```kotlin import kotlin.properties.ObservableProperty import kotlin.reflect.KProperty class BoundedInt(initial: Int, private val range: IntRange) : ObservableProperty<Int>(initial) { override fun beforeChange(p: KProperty<*>, old: Int, new: Int): Boolean = new in range // reject out-of-range override fun afterChange(p: KProperty<*>, old: Int, new: Int) { println("${p.name}: $old -> $new") // react } } class Slider { var value: Int by BoundedInt(0, 0..100) } ``` Alternatively implement `ReadWriteProperty<R, T>` directly when you need full control (e.g. compute-on-get, thread-safety, transform the stored value — something the before/after hooks cannot do since they can't change `new`). ## Why KProperty in the hooks Each hook receives `property: KProperty<*>` so the same delegate can be reused across many properties and still report `property.name`, read annotations, etc. This is what makes generic delegates possible. ## Operator-function contract Any delegate ultimately works because the compiler looks for `getValue`/`setValue` operator functions (provided here via `ReadWriteProperty`). `provideDelegate` (a sibling topic) can additionally intercept delegate creation, but the observable family does not use it.
- In ObservableProperty.setValue, what is the order of beforeChange, the field write, and afterChange?beforeChange runs first; if it returns true the field is written, then afterChange runs. If beforeChange returns false, the method returns early and nothing else happens.
- Can the before/after hooks transform the value being stored?No — they only see old/new and (for beforeChange) accept or reject. To transform, implement ReadWriteProperty.setValue directly.
- Does notNull() share the ObservableProperty base?No, it is a separate NotNullVar implementation of ReadWriteProperty that throws on read-before-init.
saying these in an interview costs you the question
- Claiming observable and vetoable are unrelated classes
- Saying beforeChange can modify the new value
- Thinking notNull extends ObservableProperty
- Not knowing the setValue ordering (before -> write -> after)
- Forgetting that full transform/control needs ReadWriteProperty directly