Search code examples
kotlinkotlin-interop

Kotlin constructor delegation to inner data class?


We have an abstract Java class (which we can't modify) called AbstractClass that we want to implement in Kotlin. A requirement is the Kotlin implementation is serializable/deserializable to JSON using vanilla Jackson Databind. This has lead us to the following implementation:

class MyClass(private val data: MyClassData? = null) : AbstractClass<MyClassData>(MyClass::class.java, "1") {

    data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData

    override fun getData(): MyClassData? {
        return data
    }
}

This class will always be used from Java and currently you can instantiate it like this (Java):

MyClass myClass = new MyClass(new MyClassData("John Doe", 25));

But we'd prefer to instantiate it like this instead:

MyClass myClass = new MyClass("John Doe", 25);

I can of course change the Kotlin code to something like this:

class MyClass(@JsonIgnore private var name: String = "", @JsonIgnore private var age: Int = 0) : AbstractClass<MyClassData>(MyClass::class.java, "1") {

    data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData

    private var data : MyClassData? = null

    init {
        data = MyClassData(name, age)
    }

    override fun getData(): MyClassData? {
        return data
    }
}

but this is very verbose and kind of defeats the purpose of using Kotlin.

What I think I'd like to do is something like this (pseudo code):

class MyClass(private val data: MyClassData? = null by MyClassData) : AbstractClass<MyClassData>(MyClass::class.java, "1") {

    data class MyClassData(var name: String = "", var age: Int = 0) : AbstractData

    override fun getData(): MyClassData? {
        return data
    }
}

(note the by MyClassData in the MyClass constructor which obviously doesn't work)

I.e. I'd like to somehow destruct or delegate the constructor of MyClass to take the same arguments as MyClassData without duplicating them. Is this something you can do in Kotlin or is there another way to solve it without adding too much code?


Solution

  • I think your main concerns are: (a) concise external API, (b) clean internal state (for Jackson)

    Secondary Constructor

    This is pretty lean:

    class MyClass internal constructor(private val data: MyClassData)
        : AbstractClass<MyClass>(MyClass::class.java, "1") {
    
        data class MyClassData(var name: String, var age: Int) : AbstractData
    
        constructor(name: String, age: Int) : this(MyClassData(name, age))
    
        override fun getData(): MyClassData? = data
    
    }
    

    Simple API without creating extra fields (though I think this syntax is misleading):

    val myClass = MyClass("John Doe", 25)
    

    Pass-Thru Params & Initializer:

    This was my first idea: directly pull out the outer class' params (though I now think the secondary constructor is nicer since it doesn't pollute the outer class):

    class MyClass(@JsonIgnore private val name: String, @JsonIgnore private val age: Int)
        : AbstractClass<MyClass>(MyClass::class.java, "1") {
    
        data class MyClassData(var name: String, var age: Int) : AbstractData
    
        private val data = MyClassData([email protected], [email protected])
    
        override fun getData(): MyClassData? = data
    
    }
    

    ...again, same API:

    val myClass = MyClass("John Doe", 25)
    

    Self-Factory

    This approach has potentially more descriptive syntax:

    class MyClass(private val data : MyClassData)
        : AbstractClass<MyClass>(MyClass::class.java, "1") {
    
        data class MyClassData(var name: String, var age: Int) : AbstractData
    
        companion object Factory {
            fun create(name: String, age: Int) = MyClass(MyClassData(name, age))
        }
    
        override fun getData(): MyClassData? = data
    
    }
    

    This can be called like this:

    val myClass = MyClass.Factory.create("John Doe", 25)
    

    Conceptual Syntax: Structuring (DOES NOT EXIST)

    I kind of like the idea of a 'structuring' syntax for method arguments that would group inputs into an object (the opposite of destructuring); a bit like varargs (i.e. syntactic sugar):

    class MyClass(
        private val data: (name: String, age: Int) : MyClassData(name, age)
    ) { ... }
    

    This could be called in either of two ways:

    val myClass1 = MyClass(MyClassData("John Doe", 25))
    val myClass2 = MyClass("John Doe", 25)
    

    In practice it's a rare requirement, and easily manageable with explicit overloads for just a few extra chars, so I don't think it will ever happen.