Sunday, 13 August 2023

Delegation in Kotlin

In my previous post I wrote about how to automate delegation in Python in a way similar to what is featured in Kotlin. As I said delegation is cool, as it favours composition over inheritance, but the examples in the Kotlin documentation do not cover a use case that I think is really common (and it's what makes delegation really useful). So we delegate part of the functionality of one class to another class, but we want that delegated functionality to have access to data in the main class. How would we manage that in Kotlin?

So we have a class to which we want to add a print functionality, we'll do it printable by delegating to a class that implements the Printable interface. Using delegation rather that Interface default method implementations we are able to switch from one Printer implementation to another, that is the beautiful thing of delegation. I've come up with this implementation:


interface Printable {
    var data: Reportable?
    fun print(): String
}

interface Reportable {
    var name: String
    var txt: String
}

class SimplePrinter(val ch: String) : Printable {
    override var data: Reportable? = null
    override fun print(): String = "${ch} ${data?.name}\n ${ch}${ch} ${data?.txt}\n" 
}

class AnotherPrinter(val ch: String) : Printable {
    override var data: Reportable? = null
    override fun print(): String = "${ch}${data?.name}${ch}\n\n${data?.txt}\n" 
}

class Message(override var name: String, override var txt: String, var printer: Printable): Reportable, Printable by printer

fun main(){
    // a Message instance that delegates to SimplePrinter
    val printer1 = SimplePrinter("-")
    val message1 = Message("hello", "this is the main message", printer1)
    printer1.data = message1
    println(message1.print())
    // - hello
    // -- this is the main message



    // a Message instance that delegates to AnotherPrinter
    val printer2 = AnotherPrinter("||")
    val message2 = Message("hello", "this is the main message", printer2)
    printer2.data = message2
    println(message2.print())
    // ||hello||

    // this is the main message

}

In the Printable interface we define a property data that represents the contract (having a "name" and a "txt" properties) of the classes that will delegate calls to Printable (so that Printable can work with that data). So for that data contract we define another interface, Reportable. We have 2 implementations of Printable that can print Reportable objects. Then we define a Message class that implements the Printable interface by delegating to a printer object provided during construction. For that, Message has to implement the Reportable interface, and right after constructing a Message instance, before any call to the delegated method can happen, we have to set the data property of the Printable interface, to point to that Message instance.

The beautiful part of delegation is that our Message class is not coupled to a specific implementation of the Printable interface (as it would happen if we were directly inheriting from one of those implementation). Above, I first create a Message instance that delegates to SimplePrinter, and then a new Message instance that delegates to AnotherPrinter.

The odd thing, is that once the delegation has been set in one object, I can not switch to another implementation. If I try to switch the delegation in my first Message instance, from SimplePrinter to AnotherPrinter, it has no effect, it continues to invoke the methods in SimplePrinter:


    message1.printer = printer2
    println(message1.print())
    // - hello
    // -- this is the main message

If we look into the code generated by the Kotlin compiler we can see that apart from the printer datafield associated to the printer property, the compiler creates a $$delegate_0 data field (that is set also to the printer instance provided in the constructor) ans it's this $$delegate_0 what is used for invoking the delegated methods. So changing later the printer property does not have effect in the delegation, as we continue to use $$delegate_0.

There is this excellent article about Kotlin delegation that also mentions this limitation.

No comments:

Post a Comment