Comments 20
В какой-то степени это напоминает статус коды в стиле С… История движется по спирали.
Исключения тоже не на ровном месте появились. В частности, если мой метод не знает, что делать с ошибкой, он должен передать ошибку на уровень выше. При исключениях я не должен делать ничего. При «результатах» я должен не забыть их правильно скомбинировать и пробросить наверх. И таких пробросов может наслоиться несколько уровней. И не забудьте тестами все это покрыть…
Исключения тоже не на ровном месте появились. В частности, если мой метод не знает, что делать с ошибкой, он должен передать ошибку на уровень выше. При исключениях я не должен делать ничего. При «результатах» я должен не забыть их правильно скомбинировать и пробросить наверх. И таких пробросов может наслоиться несколько уровней. И не забудьте тестами все это покрыть…
Основное отличие типа Result от простого кода ошибки — типобезопасность. Этот тип (точнее семейство типов Result<T, E> в общем случае) сохраняет тип ошибки, что позволяет проверять валидность кода во время компиляции. С простыми кодами ошибок в виде int это не прокатит, т.к. компилятор не знает семантическую нагрузку этого int-а.
P.S. Я не претендую на знание C#, но довольно давно пишу в функциональном стиле в т.ч. на Scala, Rust, Python и немного баловался с Haskell, так что в этой области чувствую себя относительно хорошо.
P.S. Я не претендую на знание C#, но довольно давно пишу в функциональном стиле в т.ч. на Scala, Rust, Python и немного баловался с Haskell, так что в этой области чувствую себя относительно хорошо.
Читать такой код намного проще.Мм, нет, извините. На мой дилетантский взгляд простыня из
.OnSuccess
намного менее читаема.Возможно вам просто привычнее стандартный подход. На деле пример в статье очень неудачный для пропаганды: он очень простой. В данном примере есть разница между тридцаткой строк почти линейного кода и десяткой строк совсем линейного кода. Поэтому безоговорочно выигрывает привычный код.
В случае, когда кода гораздо больше и необходима мультивалидация (т.е. нужно вернуть все ошибки) данный подход является более читабельным. По крайней мере, я самостоятельно пришел к такому подходу.
В случае, когда кода гораздо больше и необходима мультивалидация (т.е. нужно вернуть все ошибки) данный подход является более читабельным. По крайней мере, я самостоятельно пришел к такому подходу.
Чем плохи исключения для ошибок?
Да много чем плохи. Начнем с того, что исключение и ошибка — это две разные ситуации со всеми вытекающими.
Ошибка может прерывать выполнение только текущего блока и являться штатной ситуацией в блоке выше по стеку. При этом нам совсем не нужно может быть получать получать стек вызовов и тратить на это ресурсы. Если забыть обработать нужным образом ошибку, то программа в идеале не должна компилироваться, а не падать в рантайме с возможным повреждением открытых ресурсов и т.д.
А исключение, опять же в идеале, никто перехватывать не должен. Если оно возникло, то нужно доработать код.
Проблема сейчас в C#, такая же как и в java — отсутствие краткого синтаксиса для работы в функциональном стиле. Поэтому получается достаточно ущербный код, типа приведенного в статье. Но сама идея верна и при правильной реализации увеличивает производительность, безопасность и читаемость кода.
Я к похожим выводам в статье для scala прихожу habrahabr.ru/post/262971, где пытаюсь сравнивать исключения и монады для обработки ошибок. Правда, боюсь, рассуждения там могут быть немного сумбурными.
Ошибка может прерывать выполнение только текущего блока и являться штатной ситуацией в блоке выше по стеку. При этом нам совсем не нужно может быть получать получать стек вызовов и тратить на это ресурсы. Если забыть обработать нужным образом ошибку, то программа в идеале не должна компилироваться, а не падать в рантайме с возможным повреждением открытых ресурсов и т.д.
А исключение, опять же в идеале, никто перехватывать не должен. Если оно возникло, то нужно доработать код.
Проблема сейчас в C#, такая же как и в java — отсутствие краткого синтаксиса для работы в функциональном стиле. Поэтому получается достаточно ущербный код, типа приведенного в статье. Но сама идея верна и при правильной реализации увеличивает производительность, безопасность и читаемость кода.
Я к похожим выводам в статье для scala прихожу habrahabr.ru/post/262971, где пытаюсь сравнивать исключения и монады для обработки ошибок. Правда, боюсь, рассуждения там могут быть немного сумбурными.
Резюмирую.
Минусы исключений:
— хуже производительность
— отсутствует проверка уровня компиляции, что исключения формируются определенного вида и что они все преобразуются в адекватное сообщение для пользователя
— отсутствует нативная конструкция для выполнения независимых проверок с собиранием ошибок в одну агрегированную
Плюсы исключений:
— нативный синтаксис
— согласованность с другими конструкциями языка
— нативная поддержка компилятором
— нативная поддержка framework-ом и сторонними библиотеками
Минусы исключений:
— хуже производительность
— отсутствует проверка уровня компиляции, что исключения формируются определенного вида и что они все преобразуются в адекватное сообщение для пользователя
— отсутствует нативная конструкция для выполнения независимых проверок с собиранием ошибок в одну агрегированную
Плюсы исключений:
— нативный синтаксис
— согласованность с другими конструкциями языка
— нативная поддержка компилятором
— нативная поддержка framework-ом и сторонними библиотеками
отсутствует проверка уровня компиляции, что исключения формируются определенного вида и что они все преобразуются в адекватное сообщение для пользователяТесты.
Для начала, нужно понимать, что есть разного рода ошибки. Есть исключительные ситуации, а есть валидация. Исключительные ситуации очень похожи на валидацию, но это как разница между интерфейсом и абстракным классом (особенно в C++) — она на уровне семантики (смысла кода). Исключительные ситуации нужны для проверки корректности работы системы. Валидация нужна для проверки корректности данных от пользователя.
Исключительные ситуации очень хороши, когда вы применяете какую-нибудь разновидность контрактного программирования и убеждаетесь, что входные параметры у метода корректны. Попытался поделить целочисленное число на ноль? Получай исключением в морду. Тут исключения рулят и бибикают.
Теперь рассмотрим другой пример. После редактирования сущности сервер должен в любом случае провалидировать корректность данных (даже если такие проверки есть на фронтовой части). При этом пользователь должен получить список всех ошибок, иначе его будет раздражать процесс работы с системой. Но вы можете бросить только одно исключение, которое увидит пользователь. Поэтому тут исключения враг. В данном случае очень хорошо работает подход с набором различных независимых валидаторов и собиранием результатов их работы. Тот же ASP.NET MVC/Web API с их ModelState.IsValid — это классический пример данного подхода. При этом при самой валидации нам не нужен стектрейс, а иногда еще и даже вреден — вы же не хотите, чтобы до юзера дошла информация о стектрейсе приложения?
Но, у подхода с набором различных независимых валидаторов есть проблемы.
Первая проблема: они независимы друг от друга.
Пример: у вас есть поля password и confirmPassword. На первое поле навешана валидация «пароль должен быть сильным», на второе поле навешана валидация «confirmPassword должен совпадать с password». Показывать обе ошибки немного странно.
Вторая проблема: они весьма криво работают как только у вас появляется мультишаговая валидация.
Пример: юзер аплоадит файл с данными. Сначала вы пытаетесь распарсить файл. В случае неуспеха вам нужно сообщить о ошибке. После парсинга файла вы уже можете валидитировать сами данные. И тут тоже нужно сообщить об ошибки. Как только таких прикольных шагов становится много — код становится очень сильно нечитаемым.
Третья проблема: они очень плохо работают когда нужно гонять запросы к бд для того, чтобы выполнить валидацию.
Пример: вы делаете штуковину, которая валидирует изменения нескольких сущностей (банальная система с выгрузкой и загрузкой данных). На одну сущность может выпасть несколько ошибок. Причем, есть некоторые ошибки, которые пожирают другие ошибки. К примеру — есть ошибка «нельзя менять данную сущность при таких-то условиях» и есть ошибки «это свойство сущности нельзя менять при таких-то условиях». Причем под каждое валидационное правило нужно подгружать разнообразные данные, что медленно. Ваш код должен показывать максимум полезных ошибок и работать минимум времени. Соберите синхрофазотрон, короче.
Прикол заключается в том, что бросить исключение это легко, привычно и требует меньше действих, поскольку интегрировано в язык. Писать набор валидаторов или Result/Validation фокусы — непривычно, да и не всегда так явно и очевидно в рамках языка. Поэтому пока что разумным подходом является смешивание подходов. Но не стоит забывать о том, что требования рано или поздно могут измениться, а времени на рефакторинг не будет.
Исключительные ситуации очень хороши, когда вы применяете какую-нибудь разновидность контрактного программирования и убеждаетесь, что входные параметры у метода корректны. Попытался поделить целочисленное число на ноль? Получай исключением в морду. Тут исключения рулят и бибикают.
Теперь рассмотрим другой пример. После редактирования сущности сервер должен в любом случае провалидировать корректность данных (даже если такие проверки есть на фронтовой части). При этом пользователь должен получить список всех ошибок, иначе его будет раздражать процесс работы с системой. Но вы можете бросить только одно исключение, которое увидит пользователь. Поэтому тут исключения враг. В данном случае очень хорошо работает подход с набором различных независимых валидаторов и собиранием результатов их работы. Тот же 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 мнения совершенно полярны. Одни говорят, что это — ненужная унылая тягомотина, которая была «неуспешным экспериментом», другие (коих меньше) считают, что, напротив, это — «свет в конце тоннеля» в смысле надежности.
Я сам склонен считать, что это — отличная штука, которая здорово дисциплинирует, помогая правильно делать то, что девелоперы делать не любят, а именно — обрабатывать нештатные ситуации.
Что же касается unchecked… Есть ведь исключения, которые почти нельзя обработать и которые, вследствие этого, бессмысленно декларировать. Например, OutOfMemory. Или еще чего похуже…
А есть исключения, которые очевидно являются ошибкой разработчика и которые обрабатывать в рамках логики программы просто неправильно. Например, IllegalArgument. Неужто ловить исключение, которое является результатом того, что вы сами сунули в функцию что попало?
Sign up to leave a comment.
Functional C#: работа с ошибками