6
\$\begingroup\$

Semester is a simple enum class.

data class YearSemester(
    val year: Int,
    val semester: Semester
) {
    constructor(string: String) : this(
        string.split("-")[0].toInt(),
        Semester.valueOf(string.split("-")[1])
    )
}

The point is that I don't want to split the String string twice.

So to say:

data class YearSemester(
    val year: Int,
    val semester: Semester
) {
    constructor(string: String){
        val splitted = string.split("-")
        this(splitted[0].toInt(), Semester.valueOf(splitted[1]))
    }
}

However, the Kotlin syntax does not permit this code as the primary constructor must be called at the first of the secondary constructors.

Is there a more elegant way to handle situations like this? (Performing some complex operation before calling the primary constructor from secondary constructor)

\$\endgroup\$

3 Answers 3

4
\$\begingroup\$

Here are two more options:

  1. Write a factory function that has the same name as the class. They do this a lot in the standard library, although mostly for interfaces. The default lint rules warn about functions starting with capital letters, but not if the name matches the type it returns. For this reason, this maybe should be considered the conventional approach.
data class YearSemester(
    val year: Int,
    val semester: Semester
)

fun YearSemester(string: String): YearSemester {
    val splitted = string.split("-")
    return YearSemester(splitted[0].toInt(), Semester.valueOf(splitted[1]))
}
  1. Add an invoke operator function to its companion object. Then it can be called with the same syntax as a constructor. I've seen people do this, but it seems kind of awkward to me.
data class YearSemester(
    val year: Int,
    val semester: Semester
) {
    companion object {
        operator fun invoke(string: String): YearSemester {
        val splitted = string.split("-")
        return YearSemester(splitted[0].toInt(), Semester.valueOf(splitted[1]))
    }
}
\$\endgroup\$
1
3
\$\begingroup\$

The reason Kotlin enforces primary constructor to be called as the first thing, is to ensure that constructors don't hide lot of logic before the construction of the object.

So where do the complex operations go? Perhaps abstracted in functions which can be named accordingly to promote code readability. The function in this case could be an extension function on String as shown below:

fun String.toYearSemester(): YearSemester {
    val splitParts = this.split("-")
    return YearSemester(splitParts[0].toInt(), Semester.valueOf(splitParts[1]))
}

Alternatively the logic could be implemented as a factory function as in the answer by Tenfour4, which promotes readability as well, though maybe not as much as specifically named functions relevant to logic that they are abstracting within.

\$\endgroup\$
1
\$\begingroup\$

There are a few options:

  1. (My favorite) use a factory method. That is, put the logic in a method outside your class, and call your primary constructor from that method.
fun createYearSemester(string: String): YearSemester {
    val splitted = string.split("-")
    return YearSemester(splitted[0].toInt(), Semester.valueOf(splitted[1]))
}
  1. Don't use a data class, use a regular class and just override equals, hashCode, and toString by yourself.
class YearSemester(string: String) {
    val year: String
    val semester: Semester

    init {
        val splitted = string.split("-")
        this.year = splitted[0].toInt()
        this.semester = Semester.valueOf(splitted[1])
    }
    override fun equals(): String { ... }
    override fun hashCode(): Int { ... }
    override fun toString(): String { ... }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.