Pull to refresh

Comments 20

В какой-то степени это напоминает статус коды в стиле С… История движется по спирали.
Исключения тоже не на ровном месте появились. В частности, если мой метод не знает, что делать с ошибкой, он должен передать ошибку на уровень выше. При исключениях я не должен делать ничего. При «результатах» я должен не забыть их правильно скомбинировать и пробросить наверх. И таких пробросов может наслоиться несколько уровней. И не забудьте тестами все это покрыть…
Основное отличие типа Result от простого кода ошибки — типобезопасность. Этот тип (точнее семейство типов Result<T, E> в общем случае) сохраняет тип ошибки, что позволяет проверять валидность кода во время компиляции. С простыми кодами ошибок в виде int это не прокатит, т.к. компилятор не знает семантическую нагрузку этого int-а.

P.S. Я не претендую на знание C#, но довольно давно пишу в функциональном стиле в т.ч. на Scala, Rust, Python и немного баловался с Haskell, так что в этой области чувствую себя относительно хорошо.
При «результатах» я должен не забыть их правильно скомбинировать и пробросить наверх.
Также добавлю к ответу kstep, что правильно скомбинировать и пробросить наверх ошибку при «результатах» гораздо проще, чем при голых кодах ошибки, по крайней мере в языках типа Scala и Haskell.
Читать такой код намного проще.
Мм, нет, извините. На мой дилетантский взгляд простыня из .OnSuccess намного менее читаема.
Возможно вам просто привычнее стандартный подход. На деле пример в статье очень неудачный для пропаганды: он очень простой. В данном примере есть разница между тридцаткой строк почти линейного кода и десяткой строк совсем линейного кода. Поэтому безоговорочно выигрывает привычный код.
В случае, когда кода гораздо больше и необходима мультивалидация (т.е. нужно вернуть все ошибки) данный подход является более читабельным. По крайней мере, я самостоятельно пришел к такому подходу.
Чем плохи исключения для ошибок?
Да много чем плохи. Начнем с того, что исключение и ошибка — это две разные ситуации со всеми вытекающими.

Ошибка может прерывать выполнение только текущего блока и являться штатной ситуацией в блоке выше по стеку. При этом нам совсем не нужно может быть получать получать стек вызовов и тратить на это ресурсы. Если забыть обработать нужным образом ошибку, то программа в идеале не должна компилироваться, а не падать в рантайме с возможным повреждением открытых ресурсов и т.д.

А исключение, опять же в идеале, никто перехватывать не должен. Если оно возникло, то нужно доработать код.

Проблема сейчас в C#, такая же как и в java — отсутствие краткого синтаксиса для работы в функциональном стиле. Поэтому получается достаточно ущербный код, типа приведенного в статье. Но сама идея верна и при правильной реализации увеличивает производительность, безопасность и читаемость кода.

Я к похожим выводам в статье для scala прихожу habrahabr.ru/post/262971, где пытаюсь сравнивать исключения и монады для обработки ошибок. Правда, боюсь, рассуждения там могут быть немного сумбурными.
Резюмирую.

Минусы исключений:
— хуже производительность
— отсутствует проверка уровня компиляции, что исключения формируются определенного вида и что они все преобразуются в адекватное сообщение для пользователя
— отсутствует нативная конструкция для выполнения независимых проверок с собиранием ошибок в одну агрегированную

Плюсы исключений:
— нативный синтаксис
— согласованность с другими конструкциями языка
— нативная поддержка компилятором
— нативная поддержка framework-ом и сторонними библиотеками
отсутствует проверка уровня компиляции, что исключения формируются определенного вида и что они все преобразуются в адекватное сообщение для пользователя
Тесты.
Не всегда помогают. Они предусматривают только то, о чём подумал программист. Гораздо важнее то, о чём программист не подумал, и строгая типизацию + функциональный подход здесь спасают, т.к. заставляют компилятор «думать» о том, о чём программист (To err is human, кстати) может и забыть.
Для начала, нужно понимать, что есть разного рода ошибки. Есть исключительные ситуации, а есть валидация. Исключительные ситуации очень похожи на валидацию, но это как разница между интерфейсом и абстракным классом (особенно в C++) — она на уровне семантики (смысла кода). Исключительные ситуации нужны для проверки корректности работы системы. Валидация нужна для проверки корректности данных от пользователя.

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

Теперь рассмотрим другой пример. После редактирования сущности сервер должен в любом случае провалидировать корректность данных (даже если такие проверки есть на фронтовой части). При этом пользователь должен получить список всех ошибок, иначе его будет раздражать процесс работы с системой. Но вы можете бросить только одно исключение, которое увидит пользователь. Поэтому тут исключения враг. В данном случае очень хорошо работает подход с набором различных независимых валидаторов и собиранием результатов их работы. Тот же ASP.NET MVC/Web API с их ModelState.IsValid — это классический пример данного подхода. При этом при самой валидации нам не нужен стектрейс, а иногда еще и даже вреден — вы же не хотите, чтобы до юзера дошла информация о стектрейсе приложения?

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

Первая проблема: они независимы друг от друга.
Пример: у вас есть поля password и confirmPassword. На первое поле навешана валидация «пароль должен быть сильным», на второе поле навешана валидация «confirmPassword должен совпадать с password». Показывать обе ошибки немного странно.

Вторая проблема: они весьма криво работают как только у вас появляется мультишаговая валидация.
Пример: юзер аплоадит файл с данными. Сначала вы пытаетесь распарсить файл. В случае неуспеха вам нужно сообщить о ошибке. После парсинга файла вы уже можете валидитировать сами данные. И тут тоже нужно сообщить об ошибки. Как только таких прикольных шагов становится много — код становится очень сильно нечитаемым.

Третья проблема: они очень плохо работают когда нужно гонять запросы к бд для того, чтобы выполнить валидацию.
Пример: вы делаете штуковину, которая валидирует изменения нескольких сущностей (банальная система с выгрузкой и загрузкой данных). На одну сущность может выпасть несколько ошибок. Причем, есть некоторые ошибки, которые пожирают другие ошибки. К примеру — есть ошибка «нельзя менять данную сущность при таких-то условиях» и есть ошибки «это свойство сущности нельзя менять при таких-то условиях». Причем под каждое валидационное правило нужно подгружать разнообразные данные, что медленно. Ваш код должен показывать максимум полезных ошибок и работать минимум времени. Соберите синхрофазотрон, короче.

Прикол заключается в том, что бросить исключение это легко, привычно и требует меньше действих, поскольку интегрировано в язык. Писать набор валидаторов или Result/Validation фокусы — непривычно, да и не всегда так явно и очевидно в рамках языка. Поэтому пока что разумным подходом является смешивание подходов. Но не стоит забывать о том, что требования рано или поздно могут измениться, а времени на рефакторинг не будет.
После редактирования сущности сервер должен в любом случае провалидировать корректность данных (даже если такие проверки есть на фронтовой части). При этом пользователь должен получить список всех ошибок, иначе его будет раздражать процесс работы с системой

Если данные полноценно отвалидированы на фронтовой части (а это предпочтительно), то серверная валидация не обязана возвращать все ошибки. В такой ситуации сработавшая серверная валидация — это ошибка приложения или попытка хака системы, т.е. исключительная ситуация.
Либо выключенный джаваскрипт на мобильном браузере )
Отключальщики джаваскрипта на десктопах добрались до мобильных телефонов? Пусть идут в лес. ;)
Если рассматривать в отрыве от валидации, то исключения и ifы просто вносят дополнительную сложность в код, делая его менее линейным и более объемным. Та же Maybe монада — это всего-лишь фокус, чтобы запрятать nullcheckи и сделать код линейным и менее объемным. Никакой магии, никакого профита кроме читаемости и линейности кода. В принципе, это всего-лишь разные подходы для управления control flow. В разных ситуациях получается разные по читаемости код.
Исключение — это та же монада, но в профиль. Со следующей семантикой: (Func f, IEnumerable<Union<Result, Exception>> args) -> Union<Result, Exception>. var exception = args.FirstOrDefault(arg => arg.IsException)?.Exception; if (exception != null) return exception; return f(args.Select(arg => arg.Result));
Да, монада. Только монада выраженная через тип явно объявляет требуемую семантику компилятору, а исключение (которое может вылететь откуда угодно и когда угодно) — нет. В Java пытались это решить с помощью checked exceptions и ключевого слова throws, но побоялись пойти до конца и оставили лазейку в виде unchecked exceptions. Так что в случае исключений компилятор программисту не помощник, программист один в поле воин, а в случае явных типов (вроде Option или Result) компилятор друг и союзник.
Так вот, откуда этот подход вырос :) из ФП… Интересно.

Занятно — по поводу checked exceptions мнения совершенно полярны. Одни говорят, что это — ненужная унылая тягомотина, которая была «неуспешным экспериментом», другие (коих меньше) считают, что, напротив, это — «свет в конце тоннеля» в смысле надежности.

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

Что же касается unchecked… Есть ведь исключения, которые почти нельзя обработать и которые, вследствие этого, бессмысленно декларировать. Например, OutOfMemory. Или еще чего похуже…

А есть исключения, которые очевидно являются ошибкой разработчика и которые обрабатывать в рамках логики программы просто неправильно. Например, IllegalArgument. Неужто ловить исключение, которое является результатом того, что вы сами сунули в функцию что попало?
Checked Exceptions являются отличной штукой при наличии в языке такой вещи, как вывод типов — при условии что вывод типов автоматически подхватывает еще и исключения.
Checked exceptions по сути заувалированный Either<Error, T>, как и следует его рассматривать.
Sign up to leave a comment.

Articles