kotlin - убрать допустимость значения NULL из свойств класса

У меня есть класс с некоторыми свойствами, допускающими значение NULL

data class RequestModel(
    val description: String?
)

и функция проверки

fun validate(model: RequestModel): RequestModel{
    if(model.description == null) throw IllegalArgumentException("description must be non null")
    return model
}

После этого шага проверки мне нужен способ указать недопустимость обнуления description свойство.

Одно из решений - создать новый класс данных с ненулевым свойством. data class RequestModel(val description: String).
Но я ищу общий способ избежать создания новых классов для каждого варианта использования.

Идеальное универсальное решение:

fun validate(model: RequestModel): NoNullableField<RequestModel>

Как я могу удалить значение NULL из свойств класса со свойствами, допускающими значение NULL, обычным способом? Полезно ли использовать какой-то контракт компилятора kotlin?

2 ответа

Прежде всего, если вы хотите работать с абстрактными проверяемыми объектами, вам необходимо Validatable интерфейс:

interface Validatable {
    fun validate()
}

Вам также понадобится класс, представляющий проверенный объект:

data class Valid<out T : Validatable>(val obj: T) {
    init {
        obj.validate()
    }

    fun <U : Any> U?.mustBeValidated(): U = checkNotNull(this) {
        "${obj::class.jvmName}.validate() successfully validated invalid object $obj"
    }
}

Теперь тебе нужно Validatable.valid() функция, которая помогает создавать Valid экземпляры:

fun <T : Validatable> T.valid(): Valid<T> = Valid(this)

Вот как вы можете сделать свой RequestModel быть Validatable:

data class RequestModel(
    val description: String?
) : Validatable {
    override fun validate() {
        requireNotNull(description) { "description must be non null" }
    }
}

val Valid<RequestModel>.description get() = obj.description.mustBeValidated()

И вот как это можно использовать:

val validModel: Valid<RequestModel> = model.valid()
val notNullDescription: String = validModel.description

Вы также можете сделать Validкласс inline. Поскольку встроенные классы не могут иметьinit блоки initлогика перенесена в фабричный метод. И поскольку основной конструктор встроенного класса должен бытьpublic, конструктор отмечен @Experimental private annotation class ValidInternal что предотвращает использование незаконного конструктора:

@UseExperimental(ValidInternal::class)
fun <T : Validatable> T.valid(): Valid<T> {
    validate()
    return Valid(this)
}

@Experimental
private annotation class ValidInternal

inline class Valid<out T : Validatable> @ValidInternal constructor(
    // Validatable is used here instead of T
    // because inline class cannot have generic value parameter
    private val _obj: Validatable
) {
    @Suppress("UNCHECKED_CAST") // _obj is supposed to be T
    val obj: T
        get() = _obj as T

    fun <U : Any> U?.mustBeValidated(): U = checkNotNull(this) {
        "${obj::class}.validate() successfully validated invalid object $obj"
    }
}

Вы можете использовать отражение Kotlin, чтобы получить все свойства и проверить, не равны ли они нулю:

inline fun <reified T : Any> T.requireNoNullableProperties() = NoNullableProperties(this, T::class)

class NoNullableProperties<out T : Any>(val obj: T, clazz: KClass<T>) {
    init {
        clazz.memberProperties.forEach { prop ->
            if (prop.returnType.isMarkedNullable) {
                prop.isAccessible = true
                requireNotNull(prop.get(obj)) {
                    "${prop.name} must be not null, obj - [$obj]"
                }
            }
        }
    }

    operator fun <R> get(property: KProperty1<in T, R?>): R = requireNotNull(property.get(obj)) {
        "Extension and mutable properties can't be validated, property - [$property], obj - [$obj]"
    }
}

Пример использования:

val validated = model.requireNoNullableProperties()
val description: String = validated[RequestModel::description]

Также вы можете извлечь validated[RequestModel::description] к свойству расширения NoNullableProperties<RequestModel>:

val ValidRequestModel.description get() = get(RequestModel::description)

где ValidRequestModel является:

typealias ValidRequestModel = NoNullableProperties<RequestModel>

Пример использования:

val validated = model.requireNoNullableProperties()
val description: String = validated.description
Другие вопросы по тегам