Delegation in Kotlin by Philippe Breault

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/

1b77dd441f657f5aefb3e21283b252e6?s=128

GDG Montreal

August 28, 2019
Tweet

Transcript

  1. Kotlin Delegation Philippe Breault @pbreault

  2. The Speaker • Philippe Breault • Android since 2010 •

    Sr. Software Engineer @ American Express
  3. 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
  4. What is Delegation Ask someone else to do the work

    for you — Phil
  5. 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() }
  6. Class Delegation

  7. Class Delegation Code sharing without inheritance ✴ So let’s start

    with inheritance !
  8. Square Rectangle area = width * height Height Width Width

    == Height Width != Height Side
  9. Inheritance Square Rectangle Shape + area() : Int

  10. Inheritance Square Rectangle Shape + area() : Int

  11. 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)
  12. 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)
  13. 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)
  14. 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
  15. 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. “?”
  16. 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. “?”
  17. 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)
  18. 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)
  19. 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)
  20. 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() }
  21. 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(); } }
  22. 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 {...} }
  23. 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 {...} }
  24. 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
  25. Property Delegation

  26. 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
  27. Lazy val lazyDelegate by lazy { computeSomethingExpensive() }

  28. Observable var spied by observable(10) { property, oldValue, newValue ->

    println("changed from $oldValue to $newValue") }
  29. Vetoable var age by vetoable(initialValue = 10) { _, _,

    newValue -> newValue > 0 }
  30. NotNull var someInt by notNull<Int>()

  31. Custom Property Delegates

  32. Custom Delegates • ReadOnlyProperty ( val ) • ReadWriteProperty (

    var ) • ObservableProperty ( var)
  33. 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) //... }
  34. 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) //... }
  35. 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) //... }
  36. 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) //... }
  37. 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; } }
  38. 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]); }
  39. 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" )) };
  40. 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 } }
  41. 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 } }
  42. 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 } }
  43. 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 } }
  44. 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 } }
  45. 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 }
  46. 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 }
  47. 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 }
  48. 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
  49. 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] } }
  50. 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
  51. Recap

  52. 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() }
  53. 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
  54. Thank You Philippe Breault @pbreault