Kotlin offers many exciting features. In general, developers tend to cite null safety as their favorite. For me, it's function extensions. But delegation comes a close second.
The delegation pattern
The delegation pattern is described in the GoF book:
Delegation is a way to make composition as powerful for reuse as inheritance [Lie86, JZ91]. In delegation, two objects are involved in handling a request: a receiving object delegates operations to its delegate. This is analogous to subclasses deferring requests to parent classes. But with inheritance, an inherited operation can always refer to the receiving object through the
this
member variable in C++ andself
in Smalltalk. To achieve the same effect with delegation, the receiver passes itself to the delegate to let the delegated operation refer to the receiver.
Delegation is critical when one chooses composition over inheritance.
Manual and native delegation
In Java, you need to code delegation manually. The example above translates into the following code:
interface A { void foo(); } class B implements A { @Override public void foo() { } } class Composition implements A { private final B b; Composition(B b) { this.b = b; } @Override public void foo() { b.foo(); } }
Kotlin handles the delegation natively using the keyword by
. You can write the same code in Kotlin like this:
interface A { fun foo() } class B : A { override fun foo() {} } class Delegate(b: B) : A by b // 1
- With this, you can call
foo()
on anyDelegate
instance
As explained in the docs:
The by-clause in the supertype list for
Delegate
indicates thatb
will be stored internally in objects ofDelegate
, and the compiler will generate all the methods ofB
that forward tob
.-- Delegation
Delegated properties
Kotlin also offers delegated properties, a property that delegates its getter (and its setter if a var
) to "something else". A delegated property also uses the by
keyword.
A couple of out-of-the-box delegates are available through the standard library.
- Non-null delegate: A non-
null
delegate behaves the same way as thelazyinit
keyword: if one uses the variable before one has initialized it to a nonnull
value, it will throw anIllegalStateException
.var notNull: String by Delegates.notNull()
- Lazy delegate: A lazy delegate computes the value on the first access, stores it, and then returns the stored value. As its name implies, you use
lazy
when the value is expensive to compute and doesn't change after computation.val lazy: String by lazy { "An expensive computation" }
- Observable: An observable delegate offers a hook when the value is accessed so you can execute code afterward.
val observed = "Observed" val observable: String by Delegates.observable(observed) { _, old, new -> println("old: $old, new: $new") }
- Vetoable: A vetoable delegate is the opposite of the observable. It offers a hook that executes before. If this hook returns
true
, the set of the value executes as expected; if it returnsfalse
, the set doesn't happen.val vetoable: String by Delegates.vetoable(observed) { _, _, _ -> Random.nextBoolean() }
Here, the set fails randomly 50% of the time. It's not helpful but fun to debug for your colleagues.
Your own delegated property
If you want to create your own delegated property, it needs to point to a class that has:
- An
operator fun getValue(thisRef: T, prop: KProperty): U
operator function for fields whose value is immutable - An extra
operator fun getValue(thisRef: T, prop: KProperty, value: U)
if it's mutable
T
is the class' typeU
the property'sthisRef
is the class instancevalue
is the new valueprop
is the property itself
As an illustration, let's implement a distributed cache delegated property based on Hazelcast IMDG.
class HazelcastDelegate<T>(private val key: String) { private val map: IMap<String, Any> by lazy { // 1 val config = Config().apply { instanceName = "Instance" } Hazelcast.getOrCreateHazelcastInstance(config).getMap("values") } operator fun getValue(thisRef: T, prop: KProperty<*>) = map[key] // 2 operator fun setValue(thisRef: T, prop: KProperty<*>, value: Any?) { map[key] = value // 3 } }
- Create a reference to a Hazelcast
IMap
- Get the value from the
IMap
- Set the value in the
IMap
Using the above delegate is straightforward:
class Foo { var cached: Any? by HazelcastDelegate<Foo>("cached") } fun main() { val foo = Foo() foo.cached = "New value" println(foo.cached) }
Conclusion
The delegate pattern is ubiquitous in the Object-Oriented Programming world. Some languages, such as Kotlin, provides a native implementation.
But delegation doesn't stop at the class level. Kotlin does provide delegation at the property level. It provides some out-of-the-box delegates, but you can easily create your own.
Original published at A Java Geek on April 18th, 2021