Параметры класса данных валидации Котлин

Если я моделирую свои объекты значений с использованием классов данных Kotlin, то какой метод лучше всего подходит для проверки. Кажется, что блок init является единственным логическим местом, поскольку он выполняется после основного конструктора.

data class EmailAddress(val address: String) {

    init {
        if (address.isEmpty() || !address.matches(Regex("^[a-zA-Z0-9][email protected][a-zA-Z0-9]+(.[a-zA-Z]{2,})$"))) {
            throw IllegalArgumentException("${address} is not a valid email address")
        }
    }
}

Использование примера JSR-303

Недостатком этого является то, что требуется ткать время загрузки

@Configurable
data class EmailAddress(@Email val address: String) {

    @Autowired
    lateinit var validator: Validator

    init {
        validator.validate(this)
    }
}

Ответ 1

На самом деле, похоже, что валидация не несет ответственности за классы данных. data говорит сам за себя - он используется для хранения данных.

  • Итак, если вы хотите проверить класс данных, будет иметь смысл установить проверку @get: на аргументы конструктора и проверить вне класса данных в классе, ответственного за построение.

  • Второй вариант заключается не в том, чтобы использовать класс данных, просто использовать простой класс и реализовать целую логику в конструкторе, проходящем валидатор там

  • Кроме того, если вы используете Spring Framework - вы можете сделать этот класс Bean с областью прототипа, но, скорее всего, будет абсолютно неудобно работать с таким типом спагетти-кода:)

Ответ 2

Я сделал комментарий, но подумал, что вместо этого поделюсь своим подходом к валидации.

Во-первых, я считаю ошибкой выполнять проверку при создании экземпляра. Это сделает границу между десериализацией и передачей контроллерам грязной. Кроме того, для меня, если вы придерживаетесь чистой архитектуры, валидация является частью вашей основной логики, и вы должны убедиться, что тесты на вашей основной логике это происходит.

Итак, чтобы позволить мне заняться этим так, как я хочу, я сначала определю свой собственный основной API проверки. Чистый котлин. Нет рамок или библиотек. Держи в чистоте.

interface Validatable {

    /**
     * @throws [ValidationErrorException]
     */
    fun validate()
}


class ValidationErrorException(
        val errors: List<ValidationError>
) : Exception() {

    /***
     * Convenience method for getting a data object from the Exception.
     */
    fun toValidationErrors() = ValidationErrors(errors)
}

/**
 * Data object to represent the data of an Exception. Convenient for serialization.
 */
data class ValidationErrors(
        val errors : List<ValidationError>
)

data class ValidationError(
        val path: String,
        val message: String
)

Тогда у меня есть конкретные рамки реализации. Например, реализация javax.validation.Validation:

open class ValidatableJavax : Validatable {

    companion object {
        val validator = Validation.buildDefaultValidatorFactory().validator!!
    }

    override fun validate() {
        val violations = validator.validate(this)
        val errors = violations.map {
            ValidationError(it.propertyPath.toString(), it.message)
        }.toMutableList()
        if (errors.isNotEmpty()) {
            throw ValidationErrorException(errors = errors)
        }
    }
}

Единственная проблема в том, что javax-аннотации не очень хорошо работают с объектами данных kotlin - но вот пример класса с проверкой:

import javax.validation.constraints.Positive

class MyObject(
        myNumber: BigDecimal
) : ValidatableJavax() {

    @get:Positive(message = "Must be positive")
    val myNumber: BigDecimal = myNumber

}