3

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?

Johan
  • 37,479
  • 32
  • 149
  • 237
  • Have you tried: `data class MyClassData(var name: String = this@MyClass.name, var age: Int = this@MyClass.age) : AbstractData`, with name & age added to the outer constructor params (untested) – charles-allen Aug 17 '17 at 17:55

1 Answers1

3

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(this@MyClass.name, this@MyClass.age)

    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.

charles-allen
  • 3,891
  • 2
  • 23
  • 35
  • Thanks, it's a bit better than my suggestion but it still means that I have to duplicate all constructor variables in both the outer and inner class which is what I like to avoid if possible. Also in this scenario the "name" and "age" from MyClass will be included in the generated JSON by Jackson (which is not what I want). – Johan Aug 18 '17 at 12:21
  • @Johan - You can retag with @JsonIgnore... I was focusing on your primary objective. WRT the constructor, you have to pass the data in somehow, either packaged as an object or not. You said you don't like calling the inner constructor so I went this route. You could use `mapOf("name" to "bob", "age" to 23)` as input, but this makes a Map which is ugly to deal with internally. I guess you could also use a kind of builder with a fluent API: `Builder(outerparams).withChild(innerParams).build()`. I think all of these options are more verbose and less desirable. – charles-allen Aug 19 '17 at 00:38
  • @Johan - I've added a self-factory method on the companion object which avoids adding redundant state and keeps the external API a bit cleaner. You could extend this to take params for the outer class as well if you desire. – charles-allen Aug 19 '17 at 00:49
  • ...and finally I realised that a secondary constructor would be a much more sensible way to approach this (since it doesn't impose extra state). – charles-allen Aug 19 '17 at 03:09