skip to content

Explain the internal design behind observable, vetoable, and notNull. How would you build a custom delegate that both validates and reacts?

level: seniorimportance: should knowfreq 25%

answer

  1. ObservableProperty: beforeChange (gate) + afterChange (react)
  2. setValue: beforeChange -> write -> afterChange
  3. false from beforeChange = early return = veto
  4. notNull = separate NotNullVar, not ObservableProperty
  5. Subclass both hooks to validate AND react

basics

~20 s

observable 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 s

observable 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 lines
kotlin
import 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

for a junior

Can describe at a high level that there's a hook before and after the change.

for a middle

Names beforeChange/afterChange and that vetoable/observable override one each.

for a senior

Reproduces the setValue ordering, builds a custom ObservableProperty overriding both hooks, and knows the hooks can't transform values.

for a principal

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

context