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

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/

GDG Montreal

August 28, 2019
Tweet

More Decks by GDG Montreal

Other Decks in Programming

Transcript

  1. Kotlin Delegation
    Philippe Breault
    @pbreault

    View Slide

  2. The Speaker
    • Philippe Breault
    • Android since 2010
    • Sr. Software Engineer @ American Express

    View Slide

  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

    View Slide

  4. What is Delegation
    Ask someone else to do the work for you — Phil

    View Slide

  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()
    }

    View Slide

  6. Class Delegation

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

  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

    View Slide

  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. “?”

    View Slide

  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. “?”

    View Slide

  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)

    View Slide

  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)

    View Slide

  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)

    View Slide

  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()
    }

    View Slide

  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();
    }
    }

    View Slide

  22. class MyLoggingList(val delegate: MutableList = mutableListOf())
    : MutableList 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): Boolean {...}
    // override fun addAll(elements: Collection): Boolean {...}
    }

    View Slide

  23. class MyLoggingList(val delegate: MutableList = mutableListOf())
    : MutableList 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): Boolean {...}
    // override fun addAll(elements: Collection): Boolean {...}
    }

    View Slide

  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

    View Slide

  25. Property Delegation

    View Slide

  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

    View Slide

  27. Lazy
    val lazyDelegate by lazy {
    computeSomethingExpensive()
    }

    View Slide

  28. Observable
    var spied by observable(10) { property, oldValue, newValue ->
    println("changed from $oldValue to $newValue")
    }

    View Slide

  29. Vetoable
    var age by vetoable(initialValue = 10) { _, _, newValue ->
    newValue > 0
    }

    View Slide

  30. NotNull
    var someInt by notNull()

    View Slide

  31. Custom Property Delegates

    View Slide

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

    View Slide

  33. ReadOnlyProperty
    // Declaration
    class LogOnRead(private val initialValue: T) : ReadOnlyProperty {
    override fun getValue(thisRef: Any, property: KProperty<*>): T {
    println("accessing $initialValue")
    return initialValue
    }
    }
    // Usage
    class MyClass {
    val w by LogOnRead(initialValue = 10)
    //...
    }

    View Slide

  34. ReadOnlyProperty
    // Declaration
    class LogOnRead(private val initialValue: T) : ReadOnlyProperty {
    override fun getValue(thisRef: Any, property: KProperty<*>): T {
    println("accessing $initialValue")
    return initialValue
    }
    }
    // Usage
    class MyClass {
    val w by LogOnRead(initialValue = 10)
    //...
    }

    View Slide

  35. ReadOnlyProperty
    // Declaration
    class LogOnRead(private val initialValue: T) : ReadOnlyProperty {
    override fun getValue(thisRef: Any, property: KProperty<*>): T {
    println("accessing $initialValue")
    return initialValue
    }
    }
    // Usage
    class MyClass {
    val w by LogOnRead(initialValue = 10)
    //...
    }

    View Slide

  36. ReadOnlyProperty
    // Declaration
    class LogOnRead(private val initialValue: T) : ReadOnlyProperty {
    override fun getValue(thisRef: Any, property: KProperty<*>): T {
    println("accessing $initialValue")
    return initialValue
    }
    }
    // Usage
    class MyClass {
    val w by LogOnRead(initialValue = 10)
    //...
    }

    View Slide

  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;
    }
    }

    View Slide

  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]);
    }

    View Slide

  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"
    ))
    };

    View Slide

  40. ReadWriteProperty
    class LogOnReadWrite(initialValue: T) : ReadWriteProperty {
    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
    }
    }

    View Slide

  41. ReadWriteProperty
    class LogOnReadWrite(initialValue: T) : ReadWriteProperty {
    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
    }
    }

    View Slide

  42. ReadWriteProperty
    class LogOnReadWrite(initialValue: T) : ReadWriteProperty {
    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
    }
    }

    View Slide

  43. ReadWriteProperty
    class LogOnReadWrite(initialValue: T) : ReadWriteProperty {
    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
    }
    }

    View Slide

  44. ReadWriteProperty
    class LogOnReadWrite(initialValue: T) : ReadWriteProperty {
    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
    }
    }

    View Slide

  45. ObservableProperty
    class LogOnWrite(initialValue: T) : ObservableProperty(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
    }

    View Slide

  46. ObservableProperty
    class LogOnWrite(initialValue: T) : ObservableProperty(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
    }

    View Slide

  47. ObservableProperty
    class LogOnWrite(initialValue: T) : ObservableProperty(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
    }

    View Slide

  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

    View Slide

  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]
    }
    }

    View Slide

  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

    View Slide

  51. Recap

    View Slide

  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() }

    View Slide

  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

    View Slide

  54. Thank You
    Philippe Breault
    @pbreault

    View Slide