Как смоделировать результат валидации нескольких полей так, чтобы клиент получил все ошибки за один проход, а не только первую?
Для пакетной валидации нужно не выбрасывать ошибку при первом нарушении, а накапливать найденные проблемы и помещать их в одну агрегированную ошибку. В Swift обычно используют Result<Success, ValidationError>, где ValidationError содержит коллекцию диагностических ошибок.
Механизм throws хорошо подходит для отказа операции по принципу «первая ошибка прерывает выполнение». Такой подход удобен для последовательных операций, но недостаточен для форм, конфигураций и импортируемых данных, где пользователю важно увидеть все исправления сразу.
Result представляет успех или неуспех как значение. Это позволяет явно описать структуру неуспеха, включая несколько причин, и передать их дальше без немедленного перехвата.
Если валидатор вызывает throw при первом неверном поле, клиент получает только одну ошибку за запуск. После её исправления обнаружится следующая, что ухудшает пользовательский опыт и приводит к повторным циклам проверки.
Простая замена throws на Result сама по себе проблему не решает: в failure всё равно нужно положить агрегат, содержащий все найденные нарушения. Важно также различать отсутствие ошибок и частично корректный результат: обычно при наличии хотя бы одной ошибки успешное значение не возвращают.
Определите отдельный тип для одной диагностической проблемы и отдельный тип для их агрегирования. Валидатор проходит по всем проверяемым полям, добавляет нарушения в массив, а в конце возвращает либо успешное значение, либо одну ValidationError с полным списком.
Ключевой механизм здесь — разделение сбора диагностик и завершения операции. Ошибка моделируется как структурированные данные, поэтому клиент может обработать каждый элемент по типу, не анализируя текст сообщения.
У такого подхода есть компромисс: валидатор должен быть рассчитан на независимые проверки. Если результат одной проверки необходим для следующей или дальнейшее выполнение небезопасно, следует остановиться раньше и использовать обычный throws или немедленный failure.
Сервис принимает профиль пользователя и должен показать все ошибки формы одновременно. Вариант с throws проще: каждая проверка может немедленно завершить функцию, но пользователь будет исправлять поля по одному. Вариант с набором строк проще реализовать, однако он теряет типобезопасность и затрудняет локализацию.
Выбранное решение — агрегированная ValidationError с типизированными Issue. Оно сохраняет типобезопасность, позволяет локализовать сообщения на уровне интерфейса и возвращает все независимые нарушения за один проход. Результатом становится либо полностью проверенный объект, либо полный набор причин отказа.
Result?Нет, не любой массив подходит. В стандартном Result<Success, Failure> тип Failure должен соответствовать Error, а массив сам по себе Error не реализует. Поэтому нужен обёрточный тип, например структура ValidationError, содержащая массив типизированных проблем.
Такой API создаёт неоднозначность: клиент может случайно использовать объект, который не прошёл обязательную проверку. Если частичный результат действительно полезен, его следует явно моделировать отдельным типом и документировать допустимые состояния; для обычной валидации безопаснее возвращать либо полный успех, либо агрегированную ошибку.
Когда проверки зависят друг от друга, имеют побочные эффекты или продолжение после первой ошибки может быть небезопасным. Например, если нельзя выполнить вторую проверку без успешно загруженных данных из первой, нужно использовать последовательную fail-fast обработку, а не собирать формальный список независимых ошибок.