JavaRush /Курсы /Kotlin SELF /Валидация как отдельный слой: диапазоны, “не пусто”, возв...

Валидация как отдельный слой: диапазоны, “не пусто”, возврат T?

Kotlin SELF
14 уровень , 3 лекция
Открыта

1. Зачем выделять валидацию в отдельный слой

Если вы когда-нибудь писали код в стиле «считываю строку → тут же парсю → тут же проверяю диапазон → тут же печатаю ошибку → тут же прошу повторить», то вы уже знаете, как быстро всё превращается в текстовый лабиринт. Причём лабиринт с минотавром: вы теряете логику, повторяете куски кода и потом боитесь трогать программу, потому что «вдруг оно развалится».

Идея слоя валидации проста: парсер отвечает за формат, а валидатор — за правила.

Например, строка "12" может быть корректно распарсена в Int, но при этом быть неправильной как «возраст котёнка в годах» (если мы ожидаем 0..30) или неправильной как «процент скидки» (если ожидаем 0..100). То есть парсинг прошёл, но значение всё равно «плохое» — и вот тут начинается валидация.

Чтобы держать это в голове, удобно представлять конвейер так:

flowchart TD
    A["Сырая строка из readln()"] --> B[Нормализация: trim/lowercase]
    B --> C[Парсинг: String -> T?]
    C --> D[Валидация: T -> T?]
    D --> E[Сценарий: используем значение или показываем сообщение]

Обратите внимание: и парсинг, и валидация часто возвращают …?, то есть nullable-тип. Это делает шаги конвейера «стыкуемыми»: любой шаг может сказать «не получилось» через null, а следующий уровень решит, что делать дальше.

2. Валидатор: что это и почему он возвращает T?

Валидатор — это маленькая функция, которая получает уже распарсенное значение и проверяет его по правилу. Если правило выполняется — возвращает значение (обычно то же самое). Если нет — возвращает null.

Почему именно T?? Потому что нам нужен единый сигнал «не прошло», который легко обрабатывается в Kotlin.

Например, валидатор «число в диапазоне» может иметь смысловую сигнатуру:

  • вход: Int, границы min, max
  • выход: Int? — либо то же число, либо null

Ещё важная мысль: валидатор обычно должен быть тихим. Он не печатает в консоль и не решает судьбу программы. Он просто отвечает на вопрос: «валидно или нет?». Это делает его переиспользуемым. Один и тот же валидатор можно применить к возрасту, проценту скидки или количеству попыток в игре — сценарии разные, а проверка диапазона одинаковая.

И да: часто внутри валидатора получается стиль guard clauses — последовательные проверки с ранним выходом. Он читается как прямой текст: «если меньше минимума — не подходит; если больше максимума — не подходит; иначе подходит».

3. Диапазоны для чисел: in, min..max и границы

Проверка диапазона — самый частый пример валидации, потому что реальные данные почти всегда имеют границы. Возраст не может быть -500, процент не может быть 250, ширина прямоугольника в нашем учебном вводе не должна быть «0», если мы ожидаем хотя бы 1.

В Kotlin есть очень читаемая форма: x in min..max. Она использует диапазон min..max и оператор in.

Валидатор для Int в диапазоне

Начнём с простой функции:

fun validateIntInRangeOrNull(x: Int, min: Int, max: Int): Int? {
    if (x !in min..max) return null
    return x
}

Смотрите, что здесь важно: мы не печатаем «Ошибка!», мы не спрашиваем ввод заново, мы не делаем ничего драматичного. Мы просто возвращаем null, если значение не попало в диапазон.

Проверим на примере:

fun main() {
    val a = validateIntInRangeOrNull(10, 1, 100)
    val b = validateIntInRangeOrNull(0, 1, 100)

    println(a) // 10
    println(b) // null
}

Границы — это часть правила

Иногда начинающие «немного» путаются: диапазон 1..100 включает 100 или нет? В Kotlin диапазон через .. включает обе границы. То есть 100 in 1..100 — это true.

Чтобы не угадывать, полезно держать маленькую табличку:

Правило Пример Результат
x in 1..3
1 in 1..3
true
x in 1..3
3 in 1..3
true
x in 1..3
0 in 1..3
false
x in 1..3
4 in 1..3
false

Если вам нужно «строго меньше» или «строго больше», это уже другой вид правила. Но сегодня мы фиксируем базовый вариант: включающие границы.

Валидатор для Double в диапазоне

Точно так же можно валидировать Double. Тут есть тонкость: Double — вещественный тип, но проверка in min..max сработает так же понятным образом (мы сравниваем числа). В нашем курсе мы уже обсуждали, что Double бывает «капризным» для ==, но сравнения <, >, in для диапазонов — нормальная практика для пользовательских ограничений.

fun validateDoubleInRangeOrNull(x: Double, min: Double, max: Double): Double? {
    if (x < min) return null
    if (x > max) return null
    return x
}

Здесь я специально показал версию без in, чтобы вы видели, что «под капотом» идея всё та же: два сравнения. Иногда такой вариант читается даже яснее, когда работаешь с Double.

4. Строки: проверка “не пусто” после trim()

Проверка «строка не должна быть пустой» кажется простой… пока вы не увидите ввод вида " " (четыре пробела). Формально это не пустая строка, но по смыслу пользователь «ничего не ввёл». Поэтому в реальных программах правило «не пусто» почти всегда означает: сначала trim(), потом проверка пустоты.

И да, тут часто спасает простая мысль: парсер отвечает «строка → значение», а валидатор отвечает «значение подходит?». Строка — тоже значение, так что её тоже можно валидировать.

Валидатор “не пусто” для строк

fun validateNonEmptyOrNull(raw: String): String? {
    val s = raw.trim()
    if (s.isEmpty()) return null
    return s
}

Проверим:

fun main() {
    println(validateNonEmptyOrNull("Alice"))   // Alice
    println(validateNonEmptyOrNull("   "))     // null
    println(validateNonEmptyOrNull(""))        // null
}

Почему валидатор возвращает нормализованную строку

Обратите внимание: мы возвращаем s, а не raw. То есть валидатор в этом примере не только проверяет правило, но и возвращает «приведённую к нормальному виду» строку.

Это хорошая практика, когда правило и нормализация логически связаны. Если наше правило звучит «строка должна быть непустой после удаления пробелов по краям», то естественно и вернуть именно тот вариант, который мы проверяли. Тогда дальше по программе вы не таскаете лишние пробелы, и вам не приходится помнить: «ой, надо было ещё раз trim() сделать».

5. Композиция: readln() → парсинг → валидация

Самое приятное начинается, когда вы складываете всё в цепочку. Мы сделаем маленький учебный сценарий: «мини-анкета», которая просит имя и возраст, а потом печатает приветствие. Главная цель — не анкета, а то, чтобы вы увидели: валидация — отдельный слой, и его можно подключать как LEGO-кирпичик.

Парсер и валидатор не должны спорить о ролях

Сначала — чистый парсер (как мы делали в прошлой лекции):

fun parseIntOrNull(raw: String): Int? {
    val s = raw.trim()
    if (s.isEmpty()) return null
    return s.toIntOrNull()
}

Теперь валидатор:

fun validateAgeOrNull(age: Int): Int? {
    return validateIntInRangeOrNull(age, 0, 150)
}

Обратите внимание: validateAgeOrNull — это просто «алиас по смыслу». Он делает код сценария читаемее: когда вы видите validateAgeOrNull, вы не думаете «что за 0..150?», вы думаете «ага, это возраст».

Соединяем шаги: readLineparsevalidate

Мы пока не будем строить «повторный ввод» циклом (это отдельная тема и отдельная привычка), поэтому сделаем простую стратегию: если что-то не получилось — печатаем сообщение и завершаем программу.

fun readLine(prompt: String): String {
    print(prompt)
    return readln()
}

fun main() {
    val name = validateNonEmptyOrNull(readLine("Имя: "))
        ?: return println("Имя не должно быть пустым.")

    val age = parseIntOrNull(readLine("Возраст: "))
        ?.let(::validateAgeOrNull)
        ?: return println("Возраст должен быть числом от 0 до 150.")

    println("Привет, $name! Твой возраст: $age") // Привет, Alice! Твой возраст: 20
}

Здесь появляется одна маленькая «магия»: ?.let(::validateAgeOrNull). Если вы ещё не любите let, можно написать более «в лоб», без новых конструкций:

fun main() {
    val name = validateNonEmptyOrNull(readLine("Имя: "))
        ?: return println("Имя не должно быть пустым.")

    val ageRaw = readLine("Возраст: ")
    val ageParsed = parseIntOrNull(ageRaw) ?: return println("Возраст: введите целое число.")
    val age = validateAgeOrNull(ageParsed) ?: return println("Возраст: допустимый диапазон 0..150.")

    println("Привет, $name! Твой возраст: $age")
}

Да, строк чуть больше, зато всё максимально прямолинейно: распарсили, потом провалидировали. Для новичка это часто лучший вариант.

6. Как не превратить валидаторы в “комбайн”

На этом этапе обычно возникает соблазн: «А давайте валидатор будет и печатать ошибку, и сам повторно спрашивать, и вообще всё сделает за меня». Понимаю. Это как хотеть швейцарский нож, который ещё и сам готовит борщ. Но в программировании такие «комбайны» быстро становятся проблемой.

Правило, которое спасает психику: валидатор должен быть маленьким, предсказуемым и переиспользуемым. Он принимает значение, проверяет правило, возвращает либо значение, либо null. Он не должен знать, откуда значение пришло (консоль, тест, файл) и что будет дальше.

В именах тоже полезна дисциплина. Если функция может вернуть null как «не прошло», это должно быть видно из имени: суффикс OrNull — отличный договор с будущим собой. А будущий вы — это вы же, только уставший, в 2:30 ночи, перед дедлайном.

И ещё одно: старайтесь возвращать «нормализованный» вариант значения, если нормализация нужна для правила. В строках это особенно заметно: если вы проверяете trim()-версию, то логично её и вернуть, чтобы не тащить дальше пробелы.

Мини-библиотека валидаторов для текущего дня

Чтобы закрепить подход, соберём «набор кубиков», которые мы уже фактически написали. Это ещё не финальная архитектура дня (это будет в следующих лекциях), но это хороший промежуточный вид: валидаторы — отдельно, парсеры — отдельно.

fun validateIntInRangeOrNull(x: Int, min: Int, max: Int): Int? {
    if (x !in min..max) return null
    return x
}

fun validateNonEmptyOrNull(raw: String): String? {
    val s = raw.trim()
    if (s.isEmpty()) return null
    return s
}

fun validateDoubleInRangeOrNull(x: Double, min: Double, max: Double): Double? {
    if (x < min) return null
    if (x > max) return null
    return x
}

Заметьте, что проверка x in min..max — это очень «kotlin-ный» способ выразить мысль «значение внутри диапазона».

7. Типичные ошибки при выделении валидации в отдельный слой

Ошибка №1: смешивание парсинга и валидации в одну функцию на “полторы страницы”.
Очень легко начать с идеи «сделаю одну функцию readAge() и она всё решит», а через пару правок получить 40 строк, где перемешались trim(), toIntOrNull(), диапазоны, сообщения об ошибках и какие-то специальные случаи. Такой код сложно переиспользовать и почти невозможно аккуратно расширять. Разделение на «парсер» и «валидатор» выглядит как лишняя морока ровно до того момента, пока вы не добавите второе поле ввода и не поймёте, что копируете одно и то же.

Ошибка №2: валидатор печатает сообщения в консоль.
Когда валидатор начинает делать println("Ошибка!"), он теряет универсальность. Сегодня вы хотите одну формулировку ошибки, завтра — другую, послезавтра — вообще не консоль, а тесты, где вывод не нужен. Гораздо спокойнее, когда валидатор просто возвращает null, а решение «что говорить пользователю» принимает сценарий. Тогда вы не получаете «голос в голове программы», который вдруг начинает говорить не тем тоном.

Ошибка №3: “волшебные значения” вместо null.
Иногда вместо null возвращают -1 или пустую строку, считая это «проще». Проблема в том, что -1 может быть реальным значением (например, в другой задаче), а пустая строка может быть валидным вводом в каком-то контексте (например, необязательное поле). null честно означает «значение не получено / не прошло проверку», и Kotlin помогает вам не забыть обработку.

Ошибка №4: проверка “не пусто” без trim().
Если вы проверяете только raw.isEmpty(), то строка из пробелов пройдёт как «не пустая». Пользователь будет уверен, что он «ничего не ввёл», а программа — что «всё отлично, имя задано». Поэтому правило «не пусто» почти всегда надо понимать как «не пусто после удаления пробелов по краям», и возвращать уже trim()-версию.

Ошибка №5: не договориться о границах диапазона и случайно поменять смысл проверки.
Сегодня вы считаете, что возраст 150 допустим, завтра — что нет, и вы начинаете менять проверки вручную в разных местах. Вынесенный валидатор решает эту проблему: правило живёт в одном месте. И ещё важнее — код x in min..max читается однозначно: диапазон включающий границы, что снижает шанс «ошибки на единицу».

1
Задача
Kotlin SELF, 14 уровень, 3 лекция
Недоступна
Фильтр диапазона
Фильтр диапазона
1
Задача
Kotlin SELF, 14 уровень, 3 лекция
Недоступна
Имя без пробелов
Имя без пробелов
1
Задача
Kotlin SELF, 14 уровень, 3 лекция
Недоступна
Скидка по правилам
Скидка по правилам
1
Задача
Kotlin SELF, 14 уровень, 3 лекция
Недоступна
Датчик температуры
Датчик температуры
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ