Validation and DDD - kotlin data classes
Asked Answered
D

3

22

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)
Dyne answered 30/8, 2018 at 9:22 Comment(1)
Interesting write-up on approaches to Value-based classes in Kotlin: medium.com/@dev.ahmedmourad73744/…Audacity
W
14

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
Walrath answered 30/8, 2018 at 10:51 Comment(5)
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")Heins
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 stringKevyn
@Heins you won’t be able to do that if ‘name’ is a ‘val’Haiphong
@FanaSithole yes you will. The public copy method creates a new instance using the primary constructor of the data class, exposing the private constructor.Audacity
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 :) proandroiddev.com/…Hightail
A
57

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
Audacity answered 29/4, 2020 at 13:45 Comment(1)
If your class needs to be validated before instantiating it, you can use the build pattern instead. It'll give you more flexibility over the flow of your code. For instance, you could apply a fluid interface, callbacks, or even throw an exception, without compromising legacy code that might not have all the information to instantiate your data class.Klemm
W
14

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
Walrath answered 30/8, 2018 at 10:51 Comment(5)
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")Heins
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 stringKevyn
@Heins you won’t be able to do that if ‘name’ is a ‘val’Haiphong
@FanaSithole yes you will. The public copy method creates a new instance using the primary constructor of the data class, exposing the private constructor.Audacity
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 :) proandroiddev.com/…Hightail
K
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)
}
Kitts answered 13/11, 2020 at 22:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.