Share
Embed
Delegation in Kotlin by Philippe Breault
Delegation in Kotlin
Level: Beginner
by Philippe Breault, American Express
Ever heard of the phrase “Prefer composition over inheritance”? One of the better patterns to achieve this is “Delegation”. And thankfully, Kotlin has built-in support for this pattern.
This talk will cover class delegation and delegated properties as a means to make your code more readable and reusable. We’ll take a look at both approaches’ strengths, weaknesses, and when you should use them. We will also take a look at the code that is generated under the hood to discuss performance implications.
https://gdgmontreal.com/2019/07/26/kotlin-everywhere/
GDG Montreal
August 28, 2019
Transcript
54 slides
-
The Speaker • Philippe Breault • Android since 2010 •
Sr. Software Engineer @ American Express -
What is Delegation In software engineering, the delegation pattern is
an object-oriented design pattern that allows object composition to achieve the same code reuse as inheritance. — Wikipedia https://en.wikipedia.org/wiki/Delegation_pattern -
What is Delegation Ask someone else to do the work
for you — Phil -
What is Delegation https://en.wikipedia.org/wiki/Delegation_pattern class Rectangle(val width: Int, val height:
Int) { fun area() = width * height } class Window(val bounds: Rectangle) { // Delegation fun area() = bounds.area() } -
Class Delegation Code sharing without inheritance ✴ So let’s start
with inheritance ! -
Square Rectangle area = width * height Height Width Width
== Height Width != Height Side -
Inheritance interface Shape { fun area(): Float } open class
Rectangle(val width: Float, val height: Float) : Shape { override fun area() = width * height } class Square(val side: Float) : Rectangle(side, side) -
Inheritance interface Shape { fun area(): Float } open class
Rectangle(val width: Float, val height: Float) : Shape { override fun area() = width * height } class Square(val side: Float) : Rectangle(side, side) -
Inheritance interface Shape { fun area(): Float } open class
Rectangle(val width: Float, val height: Float) : Shape { override fun area() = width * height } class Square(val side: Float) : Rectangle(side, side) -
Inheritance - Problems? open class Rectangle(val width: Float, val height:
Float) : Shape { override fun area() = width * height } class Square(val side: Float) : Rectangle(side, side) Square + side : Int + height : Int + width : Int Is this the api that we want? Redundant properties? Rectangle can be extended by any class -
Inheritance - Quiz? fun main() { println(Square().toName()) } fun Shape.toName()
= when (this) { is Rectangle -> "Rectangle" is Square -> "Square" else -> "?" } open class Rectangle(…) : Shape class Square(…) : Rectangle(…) Will the answer be A. “Square” B. “Rectangle” C. “Rectangle” and “Square” D. “?” -
Inheritance - Quiz? fun main() { println(Square().toName()) } fun Shape.toName()
= when (this) { is Rectangle -> "Rectangle" is Square -> "Square" else -> "?" } open class Rectangle(…) : Shape class Square(…) : Rectangle(…) Will the answer be A. “Rectangle” B. “Square” C. “Rectangle” and “Square” D. “?” -
Class Delegation interface Shape { fun area(): Float } open
class Rectangle(val width: Float, val height: Float) : Shape { override fun area() = width * height } class Square(val side: Float) : Rectangle(side, side) -
Class Delegation interface Shape { fun area(): Float } class
Rectangle(val width: Float, val height: Float) : Shape { override fun area() = width * height } class Square(val side: Float) : Shape by Rectangle(side, side) -
Class Delegation interface Shape { fun area(): Float } class
Rectangle(val width: Float, val height: Float) : Shape { override fun area() = width * height } class Square(val side: Float) : Shape by Rectangle(side, side) -
Class Delegation class Square(val side: Float) : Shape by Rectangle(side,
side) // Is the same as: class Square(val side: Float) : Shape { private val delegate = Rectangle(side, side) override fun area() = delegate.area() } -
Class Delegation public final class Square implements Shape { private
final float side; // $FF: synthetic field private final Rectangle $$delegate_0; public final float getSide() { return this.side; } public Square(float side) { this.$$delegate_0 = new Rectangle(side, side); this.side = side; } public float area() { return this.$$delegate_0.area(); } } -
class MyLoggingList<T>(val delegate: MutableList<T> = mutableListOf()) : MutableList<T> by delegate
{ override fun add(element: T): Boolean { println("Adding $element") return delegate.add(element) } // override fun add(index: Int, element: T) {...} // override fun addAll(index: Int, elements: Collection<T>): Boolean {...} // override fun addAll(elements: Collection<T>): Boolean {...} } -
class MyLoggingList<T>(val delegate: MutableList<T> = mutableListOf()) : MutableList<T> by delegate
{ override fun add(element: T): Boolean { println("Adding $element") return delegate.add(element) } // override fun add(index: Int, element: T) {...} // override fun addAll(index: Int, elements: Collection<T>): Boolean {...} // override fun addAll(elements: Collection<T>): Boolean {...} } -
Recap - Class Delegates • Compile time operation • Hides
complexity • Keeps classes final The Good The Bad The Ugly • Can’t change the delegate after it being set up • Delegates can’t interact with overridden methods • Overriding a method can break the internal state of the delegate -
What is Property Delegation • Replacement for get()/set() var age:
Int = 100 set(value) { if (value > 0) field = value } var age: Int by vetoable(100) { _, _, newValue -> newValue > 0 } • Shareable logic using the “by” keyword -
Observable var spied by observable(10) { property, oldValue, newValue ->
println("changed from $oldValue to $newValue") } -
Vetoable var age by vetoable(initialValue = 10) { _, _,
newValue -> newValue > 0 } -
Custom Delegates • ReadOnlyProperty ( val ) • ReadWriteProperty (
var ) • ObservableProperty ( var) -
ReadOnlyProperty // Declaration class LogOnRead<T>(private val initialValue: T) : ReadOnlyProperty<Any,
T> { override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $initialValue") return initialValue } } // Usage class MyClass { val w by LogOnRead(initialValue = 10) //... } -
ReadOnlyProperty // Declaration class LogOnRead<T>(private val initialValue: T) : ReadOnlyProperty<Any,
T> { override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $initialValue") return initialValue } } // Usage class MyClass { val w by LogOnRead(initialValue = 10) //... } -
ReadOnlyProperty // Declaration class LogOnRead<T>(private val initialValue: T) : ReadOnlyProperty<Any,
T> { override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $initialValue") return initialValue } } // Usage class MyClass { val w by LogOnRead(initialValue = 10) //... } -
ReadOnlyProperty // Declaration class LogOnRead<T>(private val initialValue: T) : ReadOnlyProperty<Any,
T> { override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $initialValue") return initialValue } } // Usage class MyClass { val w by LogOnRead(initialValue = 10) //... } -
ReadOnlyProperty - Generated Code public final class LogOnRead implements ReadOnlyProperty
{ private final Object initialValue; public Object getValue(@NotNull Object thisRef, @NotNull KProperty property) { Intrinsics.checkParameterIsNotNull(thisRef, "thisRef"); Intrinsics.checkParameterIsNotNull(property, "property"); System.out.println("accessing " + this.initialValue); return this.initialValue; } public LogOnRead(Object initialValue) { this.initialValue = initialValue; } } -
ReadOnlyProperty - Generated Code // Kotlin val myProp by LogOnRead(initialValue
= “Hello”) // Java @NotNull private final LogOnRead myProp$delegate = new LogOnRead("Hello"); @NotNull public final String getMyProp() { return (String)this.myProp$delegate.getValue(this, $$delegatedProperties[0]); } -
ReadOnlyProperty - Generated Code // Java @NotNull private final LogOnRead
myProp$delegate = new LogOnRead("Hello"); @NotNull public final String getMyProp() { return (String)this.myProp$delegate.getValue(this, $$delegatedProperties[0]); } // $FF: synthetic field static final KProperty[] $$delegatedProperties = new KProperty[]{ (KProperty) Reflection.property1(new PropertyReference1Impl( Reflection.getOrCreateKotlinClass(MyClass.class), "myProp", "getMyProp()I" )) }; -
ReadWriteProperty class LogOnReadWrite<T>(initialValue: T) : ReadWriteProperty<Any, T> { private var
value = initialValue override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $value") return value } override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { println("changing ${this.value} to $value ") this.value = value } } -
ReadWriteProperty class LogOnReadWrite<T>(initialValue: T) : ReadWriteProperty<Any, T> { private var
value = initialValue override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $value") return value } override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { println("changing ${this.value} to $value ") this.value = value } } -
ReadWriteProperty class LogOnReadWrite<T>(initialValue: T) : ReadWriteProperty<Any, T> { private var
value = initialValue override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $value") return value } override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { println("changing ${this.value} to $value ") this.value = value } } -
ReadWriteProperty class LogOnReadWrite<T>(initialValue: T) : ReadWriteProperty<Any, T> { private var
value = initialValue override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $value") return value } override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { println("changing ${this.value} to $value ") this.value = value } } -
ReadWriteProperty class LogOnReadWrite<T>(initialValue: T) : ReadWriteProperty<Any, T> { private var
value = initialValue override fun getValue(thisRef: Any, property: KProperty<*>): T { println("accessing $value") return value } override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { println("changing ${this.value} to $value ") this.value = value } } -
ObservableProperty class LogOnWrite<T>(initialValue: T) : ObservableProperty<T>(initialValue) { override fun afterChange(property:
KProperty<*>, oldValue: T, newValue: T) { println("Changed from $oldValue to $newValue") } override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T) = true // Accept change // = false // Reject change } -
ObservableProperty class LogOnWrite<T>(initialValue: T) : ObservableProperty<T>(initialValue) { override fun afterChange(property:
KProperty<*>, oldValue: T, newValue: T) { println("Changed from $oldValue to $newValue") } override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T) = true // Accept change // = false // Reject change } -
ObservableProperty class LogOnWrite<T>(initialValue: T) : ObservableProperty<T>(initialValue) { override fun afterChange(property:
KProperty<*>, oldValue: T, newValue: T) { println("Changed from $oldValue to $newValue") } override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T) = true // Accept change // = false // Reject change } -
Will the answer be val p: String? by observable("Hello") {/*
... */} fun main() { if (!p.isNullOrEmpty()) { val firstChar = p[0] } } A. “H” B. Won’t compile C. Crash -
Will the answer be A. “H” B. Won’t compile C.
Crash val p: String? by observable("Hello") {/* ... */} fun main() { if (!p.isNullOrEmpty()) { val firstChar = p[0] } } -
Recap • Compile time operation • Hides complexity • Code
reuse for properties The Good The Bad The Ugly • Doesn’t play well with smart cast • Reflection call during class loading • Breaks expectations of what “val” means • Potential side-effects on read & writes -
Class Delegates • An alternative to inheritance • Compile time
• Invisible to the end user (mostly) class Square(val side: Float) : Shape by Rectangle(side, side) • Reuse logic for Properties • Built-in delegates for • lazy • vetoable • observable • notNull • Can build your own • The compiler generates the underlying code Property Delegates val lazyDelegate by lazy { expensive() } -
Should you use them? • Does it make the code
cleaner? • Is it simple to understand? • Is it free of side-effects ( or are they explicit ) ? • Is it worth the additional complexity? A few questions to ask