Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Delegation

Kotlin Delegation

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 mean 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.

Philippe Breault

August 28, 2019
Tweet

More Decks by Philippe Breault

Other Decks in Technology

Transcript

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

    Sr. Software Engineer @ American Express
  2. 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
  3. 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() }
  4. 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)
  5. 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)
  6. 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)
  7. 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
  8. 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. “?”
  9. 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. “?”
  10. 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)
  11. 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)
  12. 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)
  13. 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() }
  14. 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(); } }
  15. 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 {...} }
  16. 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 {...} }
  17. 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
  18. 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
  19. Observable var spied by observable(10) { property, oldValue, newValue ->

    println("changed from $oldValue to $newValue") }
  20. 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) //... }
  21. 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) //... }
  22. 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) //... }
  23. 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) //... }
  24. 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; } }
  25. 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]); }
  26. 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" )) };
  27. 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 } }
  28. 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 } }
  29. 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 } }
  30. 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 } }
  31. 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 } }
  32. 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 }
  33. 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 }
  34. 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 }
  35. 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
  36. 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] } }
  37. 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
  38. 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() }
  39. 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