Donnerstag, 11. Januar 2018

Trying to use Kotlin's companion objects to mimic constructor type constraints

Some problems just stick in my mind forever. I searched for a nice way to enforce the presence of a static method via an interface. C# has type constrtaints for this need, but I find the syntax rather ugly. For example, one could require that a static factory method is present on the class of a given instance. A problem regularly solved with factories or factory methods, or even factory methods as parameters.
In Kotlin, interfaces can have companions. However, they are not part of the contract of the interface, that means it's not abstract and you don't have to override it/can't override it. Methods defined on the companion can't be overriden either, so the following code doesn't do what one expects it does:

interface SomeInterface {
    companion object {
        fun xxx() {
            println("Base method by SomeInterface")
        }
    }
}
class MyImpl : SomeInterface {
    fun SomeInterface.Companion.xxx() {
        println("Overriden static method by MyImpl")
    }
}

fun test(impl: SomeClass.SomeInterface) {
    with(impl) {
        SomeClass.SomeInterface.xxx() // surprise here!
    }
}

This prints
...
Base method by SomeInterface
...
The corresponding bytecode piece in the test function is

GETSTATIC SomeClass$SomeInterface.Companion : LSomeClass$SomeInterface$Companion;
INVOKEVIRTUAL SomeClass$SomeInterface$Companion.xxx ()V   

Since the goal is to enforce the presence of a static method, it's likely that there is no base implementation at all, so no default implementation in the interface. In this case, we can only define a companion object in the interface without any methods. It's then possible to define interface methods for the companion object, just like fun SomeInterface.Companion.myMethod(). This is similar to include extension functions in an interface's contract, as described in my last blog post. Implementing interfaces would then need to implement this method. Here's a complete example on how to do this.


class SomeClass {

    interface SomeInterface {
        companion object
        fun SomeInterface.Companion.xxx()
    }

    class MyImpl : SomeInterface {
        override fun SomeInterface.Companion.xxx() {
            println("Overriden static method by MyImpl")
        }
    }

    class MyOtherImpl : SomeInterface {
        override fun SomeInterface.Companion.xxx() {
            println("Overriden static method by MyOtherImpl")
        }
    }

    companion object {
        fun test(impl: SomeClass.SomeInterface) {
            with(impl) {
                SomeClass.SomeInterface.xxx()
            }
        }
    }
}
fun main(args: Array<String>) {
    test(SomeClass.MyImpl())
    test(SomeClass.MyOtherImpl())
}

It will print
...
Overriden static method by MyImpl
Overriden static method by MyOtherImpl
...

The corresponding bytecode of the test function now looks like this:


GETSTATIC SomeClass$SomeInterface.Companion : LSomeClass$SomeInterface$Companion;
INVOKEINTERFACE SomeClass$SomeInterface.xxx (LSomeClass$SomeInterface$Companion;)V

So the old invokevirtual became an invokeinterface. This most likely comes with a small performance overhead. But hey, can't have everything.

This construct allows for factory-like constructor functions. Sadly, operator functions are not allowed to be declared on interfaces. That means, the constructor method has to be named create or something, leading effectively to an interface function signature of

fun SomeInterface.Companion.create() : T

and a usage like

with(impl) {
    println("Created ${SomeInterface.create().javaClass}")
}

This eliminates the sense in all this for me, because I would then rather type

println("Created ${impl.create().javaClass}")

The only worth-it syntax that would satisfy me would be this - based on an invoke operator function defined in the interface, usable through the interfaces companion object.

println("Created ${SomeInterface().javaClass}") //Not valid

Okay, at least I tried :)

Keine Kommentare:

Kommentar veröffentlichen