Pull to refresh

Comments 34

Бросать Exceptionы при валидации входящих данных в .NET это последнее что бы я делал на проекте, в котором много валидации данных. Еще лет 5 назад для этого атрибуты использовались и единый подход к валидации любых полей. И писать "private void ValidateDate(..." для каждого поля в каждой модели это не многим лучше throw Exception.
Видимо лет 5 назад тесты вы еще не писали, да и DI не использовали.
А как тесты и DI влияют на атрибутивную валидацию?
Ну видимо так, что нарушают концепцию DI. Не дают возможностей подмены для тестирования и т.д.
Подмены чего и в каком конкретно сценарии?
Несмотря на то, что обсуждали, отпишусь в новой статье. Я проведу аналогию с Java, что возможно немногим отличается.
Этот спор возвращает нужны ли checked и non-checked exception. Повсеместно бытует мнение, что checked exception лучше не использовать, и это так, если писать библиотеку, то checked exception заметно ухудшают жизнь пользователю этой библиотеки, так как он зачастую он использует библиотеку и не ждет данных ошибок, так как они предусмотрены другими ошибками.
Однако, существует случай, когда checked exception помогают, а именно при ожидаемых ошибках, например, неправильно заполненная форма или проверка. В этом случае желательно использовать checked exception и он, конечно же, специфичен для предметной области. Например, NotAvailableSeatsException extends GenericBookingException. Это даст необходимую информацию разработчику UI, что этот exception надо словить и показать, так как он является User case exception, в отличие от NullPointerException, IllegalArgumentException и других стандартных, которые должны быть non-checked и по-хорошему не должны появляется вообще.

Что касается notification, то этот механизм имеет свои плюсы и минусы по сравнению с checked business exception. Exception уже есть и их не надо придумывать, они интегрированы с IDE, представляют изначально объектно-ориентированную модель и расширяемы, они реализуют естественный pattern fail first and further checks rely on a predictable state. Главный минус exception, что для сложных валидаций, когда надо проверить все поля сразу или структурировать или когда проверок слишком много, код написанный в таком стиле уже будет неестественным.
Нет, нет и ещё раз. Не для того разработчики .NET включали исключения в CLR и мучались с их поддержкой в куче разных фич вроде async, чтобы мы опять использовали WinAPI-style с возвратом ошибок.
А вас не смущает, что те же самые разработчики .net предосмотрели целый интерфейс IValidatableObject с его поддержкой в куче фреймворков?
Только предусмотрели они его прежде всего для стандартных механизмов валидации (а ля класс Validator) которые в итоге все равно кидают исключения.
Валидация в asp.net MVC, скажем, не бросает (не бросала) исключения. И в любом другом месте, где мне надо встроиться, я могу взять валидатор, получить из него ошибки и использовать их.

В целом стандартные .net механизмы по сути тоже через уведомления ошибки собирают, фактически в статье велосипед. Я к тому что сбор ошибок через уведомления в итоге не противоречит использованию исключений для выдачи суммарного результата проверки наружу, а подход 'использовать исключения только если в программе что то пошло не так как задумано', т.е. практически отказываться от них вообще, мне кажется несколько ограничен и приводит к большому количеству велосипедов.
Допустим есть некий wcf-сервис, и в нем метод на вход принимает объект с параметрами, на выход выдает объект с данными. Вполне нормально будет после валидации бросить исключение со списком ошибок, не городить же быдлокод из ненужного класса-обертки с самим объектом и коллекцией ошибок валидации, вполне нормально будет кинуть исключение, а на клиенте уже вытаскивать список ошибок из этого исключения в обработчике.

В wcf нужно кидать FaultException (потому что он преобразуется в описанный стандартом SoapFault).
фактически в статье велосипед

Фактически, в статье описан подход, на котором построен и стандартный механизм .net, и некоторые механизмы в Java.

Я к тому что сбор ошибок через уведомления в итоге не противоречит использованию исключений для выдачи суммарного результата проверки наружу

Но зачем?

'использовать исключения только если в программе что то пошло не так как задумано', т.е. практически отказываться от них вообще

Почему вы приравниваете исключительные ситуации к "отказаться вообще"? У вас так мало исключительных ситуаций в программах?

приводит к большому количеству велосипедов.

Если использовать стандартные механизмы, то никаких особых велосипедов не будет.

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

Как мило, вы вот так взяли и навязали майкрософтовский велосипед в виде исключений всем сторонним клиентам оптом. Хотя они знать не хотят про ваше исключение — у них есть soap:fault, и он их устраивает. Поэтому бросать надо не абстрактное исключение со списком ошибок, а FaultException, типизованный вашим списком ошибок, и прописывать этот же список в FaultContract — что, в итоге, и свелось к тому же самому списку ошибок, с которого начинали.

И это все только потому, что в WCF нет удобного способа писать фильтры и возвращать произвольные типы данных: в MVC (или в WebAPI) можно было бы достигнуть того же эффекта вообще не используя исключения — там есть ModelState, который прекрасно валидируется во время биндинга, и на основе которого можно вернуть клиенту отлуп, который тот сможет корректно распарсить. Причем этот код можно написать один раз, запихнуть в фильтр, и забыть.
Во-первых я никому ничего не навязывал. Во-вторых не каждая система подразумевает наличие сторонних клиентов. В-третьих я не конкретизировал какое именно исключение должен бросать wcf сервис, или вы считаете что я разглагольствую об исключениях в wcf-сервисах не зная о существовании FaultException? В-четвертых даже любое абстрактное исключение при желании можно обернуть в FaultException, это лишь вопрос конкретной реализации и сути не меняет поскольку речь о том что в итоге бросается именно исключение и именно при ожидаемом поведении и это допустимо и адекватно.
Такое впечатление что вы пишете просто чтобы потроллить. Я просто привел пример где бросок исключения для той же валидации вполне уместен и корректен, а своими знаниями по mvc трясите пожалуйста перед кем нибудь другим, я пока приводил в пример только wcf, если с чем то конкретно несогласны пишите по существу.

Во-вторых не каждая система подразумевает наличие сторонних клиентов

И поэтому давайте мы будем в одних системах кидать исключения, а в других — нет?

В-четвертых даже любое абстрактное исключение при желании можно обернуть в FaultException

Знаете, как это мило разбирается на клиенте?

Я просто привел пример где бросок исключения для той же валидации вполне уместен и корректен

Нет, не уместен и не корректен: он там вынужден, потому что в WCF нет другого простого способа вернуть fault. Но если работать на уровне, отличном от типизованного WCF-сервиса, т.е. иметь прямой доступ к сообщению, работать с фолтами становится проще, чем с исключениями.

Но самое главное, что по факту, если не использовать нормальные исключения (т.е., использовать FaultException), то выясняется, что в WCF паттерн, описанный в статье, прекрасно реализуется без какого-либо велосипеда.
Но самое главное, что по факту, если не использовать нормальные исключения (т.е., использовать FaultException), то выясняется, что в WCF паттерн, описанный в статье, прекрасно реализуется без какого-либо велосипеда.

Вот да. Проще всего собрать все валидационные сообщения, положить их в коллекцию и только после этого бросить FaultException типизованный этой коллекцией. А на клиенте спокойно разобрать эту коллекцию валидационных сообщений как нужно.
… причем этот паттерн будет одинаков, используем мы WCF, WebAPI или MVC — отличаться будет только способ конверсии валидационных сообщений в сообщение протокола.
Не так давно столкнулся с тем, что надо было валидировать данные на уровне бизнес логики, а не на уровне представления в ASP.NET Core (3-х уровневая архитектура). После долгих раздумий решил посмотреть как же делают другие. И понравилось решение реализации валидаторов в Identity (https://github.com/aspnet/Identity), UserValidator например c IdentityResult.
Фаулер не сделал еще три простых и очевидных шага. Возможно язык, которым он оперирует, не позволил ему это сделать?

  1. Вводим конструкцию, которая является коллекцией исключений и сама по себе исключением одновременно. Это и есть его объект Notification, осталось только добавить к нему реализацию Throwable

  2. Позволяем нашему низкоуровневому коду (например — валидаторам конкретных полей) делать не throw new Exception, а yield new Exception. Таким образом валидатор либо молчит (всё ОК), либо генерирует серию исключений

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

Как итог мы имеем реализацию паттерна Notification на стандартных и привычных исключениях, получая все их преимущества.

Написать что-ли Фаулеру? Мол так и так, мистер, я тут кажется придумал еще один паттерн для вас )))
получая все их преимущества.

… и все недостатки.

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

А зачем его выбрасывать?
Чтобы поймать. Выбрасывать нужно, чтобы прокинуть сквозь уровни кода. И да — чтобы поймать. На нужном нам уровне. Исключения же?
Выбрасывать нужно, чтобы прокинуть сквозь уровни кода.

А точно нужно прокидывать через все уровни кода? А зачем?

чтобы поймать. На нужном нам уровне.

Теперь у вас все уровни выше знают про исключения, кидаемые ниже. Это точно правильно?
А точно нужно прокидывать через все уровни кода? А зачем?

В одних случаях нужно, в других — нет. Вы архитектор, вам решать.

И да, почему через ВСЕ? Вы приписываете мне слова, которые я не говорил. Прокинуть через уровни. Всплыть вверх. Но не до конца, иначе будет Uncaught Exception. Следовательно не через все уровни кода.

Теперь у вас все уровни выше знают про исключения, кидаемые ниже.

С чего вы это взяли? Уровни ни о чем не знают, они просто в нужном месте потенциально опасный код окружают try {… } И это — правильно. Я не вижу причин, почему try {… } catch {… } с указанием конкретных ожидаемых исключений может быть "неправильно".

Метафору "уровни знают про исключения" я не понимаю, извините.
В одних случаях нужно, в других — нет. Вы архитектор, вам решать.

А если я не хочу прокидывать вообще? Если у меня валидация — это нормальный flow, зачем мне тогда дополнительные сложности в объекте и накладные расходы на обработку исключений?

Я не вижу причин, почему try {… } catch {… } с указанием конкретных ожидаемых исключений может быть «неправильно».

Потому что чтобы указать исключения, надо про них знать — а это неизбежное увеличение связности уровней.
Вы странно рассуждаете. Как будто связность уровней своего же собственного кода — это нечто плохое. Внимание: не связность модулей, а уровней!

try { user = new User; user.fill(data); user.save(); } catch (DbException e) { // чё-то не то в сохранении в БД } catch (ValidateErrors e) { // валидаторы сработали }

Это интерфейс же. Исключения, бросаемые внутри try — это интерфейс кода, который там внутри. Что плохого в явном указании интерфейса?
Вы странно рассуждаете. Как будто связность уровней своего же собственного кода — это нечто плохое.

Да, это плохое. Оно может быть компенсировано хорошим, и хорошее может перевешивать.

Что плохого в явном указании интерфейса?

Ничего. Но чем многословнее интерфейс, тем дальше мы от принципа сокрытия информации.
Я не настолько хорошо учил в институте философию, чтобы поддерживать эту беседу ))
А если я не хочу прокидывать вообще?

Ну пишите без исключений. Пока еще это законодательно не запрещено. Просто с исключениями удобнее, знаете ли. Попробуйте.
Да я и пробовал, и пишу. И вот далеко не всегда исключения удобнее.
Теперь у вас все уровни выше знают про исключения, кидаемые ниже. Это точно правильно?

int value = sum(2,2);
теперь у вас все уровни знают, что функция sum возвращает int! это точно правильно?
Я с трудом могу себе представить случай, когда это неправильно.
Ровно также я с трудом могу себе представить случай, когда явное указание типа ожидаемого исключения — это неправильно.
Когда я задумался над вопросом (C++11): код ошибки или исключение? Решил, что нужно дать возможность пользователю API выбирать. В результате появился подход с опциональным возвращаемым кодом ошибки (реализацию подглядел в Boost.System), т.е. сигнатура метода стала выглядеть:

void someMethod(int arg0, const std::string& arg1, std::error_code &ec = throws());

а в теле:

clear_if(ec); // очистит код ошибки если не throws(). У меня размещаются в начале метода
...
throws_if<SomeExceptionClass>(ec, errcode, errcathegory); // заполнит код ошибки или выбросит SomeExceptionClass
return ...;

Реализацию можно посмотреть тут. Ограничение (меня оно не смущает) — конструктор исключения должен уметь принимать std::error_code.

А вообще идея такая: если пользователь хочет обработать какую-то ошибочную ситуацию — пусть обрабатывает (здесь и сейчас), но если забил — будет брошено исключение.
Only those users with full accounts are able to leave comments. Log in, please.