20

In Java I would do validation when creating constructor in domain object, but when using data class from kotlin I don't know how to make similar validation. I could do that in application service, but I want to stick to domain object and it's logic. It's better to show on example.

public class Example {

    private String name;

    Example(String name) {
        validateName(name);
        this.name = name;
    }
}

In Kotlin I have just a data class is there a way to do it similarly to Java style?

data class Example(val name: String)
Zoe
  • 27,060
  • 21
  • 118
  • 148
Surrealistic
  • 278
  • 1
  • 2
  • 8
  • Interesting write-up on approaches to Value-based classes in Kotlin: https://medium.com/@dev.ahmedmourad73744/value-based-classes-and-error-handling-in-kotlin-3f14727c0565 – Adam Millerchip Sep 10 '20 at 03:27

3 Answers3

54

You can put your validation code inside an initializer block. This will execute regardless of whether the object was instantiated via the primary constructor or via the copy method.

data class Example(val name: String) {
    init {
        require(name.isNotBlank()) { "Name is blank" }
    }
}

A simple example:

fun main() {
    println(Example(name = "Alice"))
    println(try { Example(name = "") } catch (e: Exception) { e })
    println(try { Example(name = "Bob").copy(name = "")  } catch (e: Exception) { e })
}

Produces:

Example(name=Alice)
java.lang.IllegalArgumentException: Name is blank
java.lang.IllegalArgumentException: Name is blank
Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74
13

You can get a similar effect by using companion factory method :

data class Example private constructor(val name: String) {
    companion object {
        operator fun invoke(name: String): Example {
            //validateName
            return Example(name)
        }
    }
}

...

val e = Example("name")
e.name //validated
Akaki Kapanadze
  • 2,552
  • 2
  • 11
  • 21
  • 14
    Though constructor is privet, you can still create `Example` with an invalid state by `copy` method of data class. E.g.: ```val ex = Example("validValue")``` and then ```ex.copy("invalidValue")``` – Teimuraz Dec 01 '18 at 23:24
  • 2
    If there is a logic which determinate if name is valid or not it's better to use `ValidatedNameString` class which makes impossible for anybody to use invalid string – Ilya Kovalyov Jul 17 '19 at 14:06
  • 1
    @Teimuraz you won’t be able to do that if ‘name’ is a ‘val’ – Fana Sithole Oct 03 '19 at 21:34
  • @FanaSithole yes you will. The public copy method creates a new instance using the primary constructor of the data class, exposing the private constructor. – Adam Millerchip Apr 27 '20 at 03:00
  • That's a bit of mind fuckery, and the link does no longer mention them. Even though I found an article explaining it, I still think the author makes a good argument for having named factory methods :) https://proandroiddev.com/til-that-you-can-use-the-invoke-operator-on-companion-objects-too-b0d21a870079 – oligofren Nov 15 '22 at 14:54
2

You may want to use the interface to hide the data class.
The amount of code will increase slightly, but I think it's more powerful.

interface Example {
    val id: String
    val name: String

    companion object {
        operator fun invoke(name: String): Example {
            // Validate ...

            return ExampleData(
                id = UUID.randomUUID().toString(),
                name = name
            )
        }
    }
    
    fun copy(name: String): Example
    
    operator fun component1() : String
    operator fun component2() : String
}

private data class ExampleData(override val id: String, override val name: String): Example {
    override fun copy(name: String): Example = Example(name)
}
Adam Millerchip
  • 20,844
  • 5
  • 51
  • 74