Validation and DDD - kotlin data classes

Viewed 9907

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)
3 Answers

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

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

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)
}
Related