Johan
Johan

Reputation: 40510

How to apply function to value defined in data class constructor before init method?

Let's say I have a data class like this:

data class MyData(val something: Int, val somethingElse : String) {
    init {
        require(something > 20) { "Something must be > 20" }
        require(StringUtils.isNotEmtpy(somethingElse)) { "Something else cannot be blank" }
    }
}

I'd like to be able to apply a function to somethingElse before the init method is called. In this case I want to remove all \n characters from the somethingElse String while maintaining immutability of the field (i.e. somethingElse must still be a val). I'd like to do something similar to this in Java:

public class MyData {

    private final int something;
    private final String somethingElse;

    public MyDate(int something, String somethingElse) {
        this.something = something;
        this.somethingElse = StringUtils.replace(somethingElse, '\n', '');

        Validate.isTrue(something > 20, "...");
        Validate.isTrue(StringUtils.isNotEmtpy(this.somethingElse), "...");
    }

    // Getters
}

I could of course create a normal class (i.e. no data class) in Kotlin but I want MyData to be a data class.

What is the idiomatic way to do this in Kotlin?

Upvotes: 3

Views: 2178

Answers (1)

Raphael
Raphael

Reputation: 10559

While you can not literally do what you want, you can fake it.

  • Make all constructors of your data class private.
  • Implement factories/builders/whatevers on the companion as operator fun invoke.

Usages of Companion.invoke will -- in Kotlin! -- look just like constructor calls.

In your example:

data class MyData private constructor(
    val something: Int, 
    val somethingElse : String
) {
    init {
        require(something > 20) { "Something must be > 20" }
        require("" != somethingElse) { "Something else cannot be blank" }
    }

    companion object {
        operator fun invoke(something: Int, somethingElse: String) : MyData =
            MyData(something, somethingElse.replace("\n", " "))
    }
}

fun main(args: Array<String>) {
    val m = MyData(77, "something\nwicked\nthis\nway\ncomes")
    println(m.somethingElse)
}

Prints:

something wicked this way comes

You'll note the helpful warning:

Private data class constructor is exposed via the generated 'copy' method.

This method can not be overridden (as far as I can tell) so you have to take care, still. One solution is to hide the actual data class away:

interface MyData {
    val s: Int
    val sE: String

    private data class MyDataImpl(
        override val s: Int,
        override val sE: String
    ) : MyData {
        init {
            require(s > 20) { "Something must be > 20" }
            require("" != sE) { "Something else cannot be blank" }
        }
    }

    companion object {
        operator fun invoke(s: Int, sE: String) : MyData =
                MyDataI(s, sE.replace("\n", " "))
    }
}

Now your invariant (no line breaks) is maintained, copy and other dangerous methods (if any, I haven't checked) are hidden away -- but therefore also unavailable, potentially removing some of the convenience data classes provide.

Choose your poison.

Upvotes: 2

Related Questions