Что такое способ исключения

Тип данных Either как альтернатива выбрасыванию исключений

Исключения – это базовый элемент многих языков программирования. Они обычно используются для обработки аномальных или непредусмотренных условий, при устранении которых необходим особый подход, нарушающий нормальный поток задач в приложении. В некоторых языках, например, в C++ или Java, исключения используются повсюду. Но не все языки спроектированы так. В C# или Kotlin нет проверяемых исключений. В других языках, например, Go и Rust, исключений нет вообще.

Думаю, код, выбрасывающий исключение всякий раз, когда произойдет что-то неожиданное, сложен для понимания, и его тяжелее поддерживать. В этой статье я хочу рассказать о типе данных Either как об альтернативном способе обработки условий, приводящих к ошибкам. Примеры в этой статье даны на Kotlin, поскольку, на мой взгляд, за его синтаксисом легко следить. Но сами концепции не уникальны для Kotlin. Их, так или иначе, можно реализовать в любом языке, поддерживающем функциональное программирование.

Различные типы ошибок

При использовании исключений требуется учитывать, какого происхождения данная ошибка, поскольку не все они равны. Некоторые ошибки, например, NullPointerException или ArrayIndexOutOfBoundsException , указывают на присутствие багов. Другие ошибки – это часть бизнес-логики. Например, если не прошла валидация, истек срок действия у токена аутентификации, либо в базе данных не нашлась нужная запись.

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

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

Как это обычно выглядит?

Давайте для примера воспользуемся сервисом бекенда, предоставляющим REST API. Простая архитектура выглядит так: тонкий контроллер вызывает сервис, который, в свою очередь, вызывает сторонний API — такая структура выглядит хорошо тестируемой и прямолинейной. Пока не столкнемся с условиями, приводящими к ошибке.

Если не обработать исключение, оно будет всплывать до тех пор, пока API не вернет ошибку 500 вызывающей стороне. Нам такого не надо.

В экосистеме SpringBoot в таких случаях принято использовать обработчик исключений. Ставите этот метод в контроллер, и он отлавливает исключения, происходящие в цепочке вызовов, сервисах и API. Тогда вызывающая сторона получает ошибку именно в том формате, в котором мы хотим ее дать.

Когда из-за исключений становится сложнее понимать поток программы

Допустим, наш сервис из вышеприведенной схемы должен проверить веб-токены JSON. Идея проста. Мы получаем JWT в виде строки и хотим знать, является ли она действительным токеном. Если является, то мы хотим получить конкретные свойства, которые обернем в TokenAuthentication . Следующий интерфейс определяет именно это:

Сигнатура, не сообщающая, какова ситуация на самом деле

Если закопаться в реализацию Verifier , то в конечном итоге найдется нечто подобное:

Как упоминалось выше, в Kotlin нет проверяемых исключений. Но это означает, что сигнатура Verifier в данном случае врет! Этот метод может выбросить исключение. Единственный способ понять, что именно происходит – посмотреть на реализацию. Тот факт, что за этой информацией приходится лезть в реализацию, явно свидетельствует, что инкапсуляции не хватает.

Явный подход

Таким образом, хочу изменить в реализации Verifier две вещи.

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

Можно использовать обнуляемый тип; в таком случае verify вернет TokenAuthentication? . Но это фиаско, братан: мы теряем всю информацию о том, что же на самом деле пошло не так. Если к ошибке могли бы привести разные причины, то информацию об этих причинах требуется сохранить.

Читайте также:  Способ применения ректально это куда

Знакомьтесь с Either (тадам. ).

Тип данных Either

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

В нашем случае Either то сущность, значение которой может быть одного из двух типов, «левое» и «правое». Есть соглашение, по которому Right соответствует успеху, а Left – ошибке. Этот паттерн распространен в сообществе функционального программирования. Он позволяет выразить тот факт, что вызов может вернуть либо корректное значение, либо ошибку, а также различать их. Но принцип именования Left/Right – это просто условность. Он может помочь тем, кто пользуется номенклатурой существующих библиотек. Можно воспользоваться иным соглашением, более понятным в вашей команде, например, Error/Success .

Можно создать простую реализацию при помощи запечатанных классов. Ниже показано, как они сочетаются с выражением when , чтобы код стал чище и в то же время безопаснее.

Давайте перепишем наш код с использованием Either .

Адаптируем интерфейс

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

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

Обертываем код, выбрасывающий исключение

Внутри нашей реализации Verifier мы обертываем проблемный код в метол расширения под названием unsafeVerify . Используем методы расширения, определенные выше, чтобы создать обе стороны Either:

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

Использование в качестве клиента

Реализация готова. Как же теперь использовать ее на вызывающей стороне? Мы хотим принимать решение о дальнейших действиях в зависимости от того, увенчались ли вычисления успехом.

Можем использовать выражение when , благодаря тому, что определили Either как запечатанный класс.

То же самое, что использовать функциональный метод fold. При помощи свертки мы выдаем результат сразу для обоих возможных значений.

Операции над значением Either

Я только что показал вам, как обращаться с Either , основываясь на двух возможных значениях (левом и правом). Но мы также оперируем значением в пределах всего приложения, и при этом не нужно каждый раз разворачивать его и обертывать обратно – в противном случае код снова стало бы сложно читать.

Нужно расширить Either двумя новыми методами, map и flatMap . Начнем с map :

Мы хотим применить функцию к тому значению, которое содержится у нас внутри Either . Either сдвинут вправо; это означает, что, как только он принимает значение Left (соответствующее ошибке), дальнейшие вычисления не применяются. Возвращаясь к нашему методу ‘ unsafeVerify , мы хотим преобразовать результат данного вызова, что и сделаем при помощи нашего нового метода map :

Нам еще остается закрыть один случай. Что, если операция, которую мы хотим применить, сама возвращает Either ? Если мы используем словарь, то вернем Either от Either и будем так вкладывать типы до тех пор, пока это не станет невозможно. Чтобы это предотвратить, воспользуемся новым методом flatMap .

Если хотите глубже покопаться в этих функциональных концепциях – почитайте эту статью.

Стрелочная библиотека

В качестве примера я привел простую реализацию Either . Гораздо лучше воспользоваться существующей реализацией. В отличной стрелочной библиотеке наряду с многими другими функциональными ништяками есть и тип Either .

Одна интересная черта библиотеки Arrow – в том, что в ней предоставляется собственная разновидность нотации do , позволяющая значительно упростить сцепленные операции над типами. В стрелочной библиотеке это называется выделение монад.

Нормальная цепочка операций могла бы выглядеть так:

Можно избавиться от синтаксиса с вложениями, и получится так:

Думаю, этот код проще читать, он напоминает синтаксис async/await, применяемый с промисами в JavaScript.

Приложение: встроенное решение

В Kotlin 1.3 и выше существует встроенный способ работы с вычислениями, которые могут не удаться. Это класс Result , обычно используемый в блоке runCatching :

Этот класс еще нельзя использовать в качестве возвращаемого типа, поэтому и в нашем интерфейсе он неприменим. Более того, Either красиво интегрируется со всем прочим функционалом стрелочной библиотеки.

Заключение

Either отлично подходит для того, чтобы сделать обработку ошибок в вашем коде более явной. Интерфейсы четче сообщают, что именно может вернуть вызов. Более того, можно безопасно сцеплять множество операций, узнавая по ходу дела, не пошло ли что-нибудь не так. Если что-то действительно пойдет не так, вычисление закоротится. Это особенно полезно при использовании конвейеров данных в рамках пакетных операций. В таком случае вы, вероятно, не будете выходить из игры при первой же ошибке, а проведете пакетную операцию полностью, аккумулировав все ее успешные и неуспешные этапы.

Читайте также:  Конский каштан способы приготовления настойки

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

Источник

Исключения метод

Энциклопедический словарь Ф.А. Брокгауза и И.А. Ефрона. — С.-Пб.: Брокгауз-Ефрон . 1890—1907 .

Смотреть что такое «Исключения метод» в других словарях:

Метод Квайна — Метод Куайна способ представления функции в ДНФ или КНФ с минимальным количеством членов и минимальным набором переменных.[1][2][3] Преобразование функции можно разделить на два этапа: на первом этапе осуществляется переход от канонической формы… … Википедия

метод упорядоченного исключения — — [Я.Н.Лугинский, М.С.Фези Жилинская, Ю.С.Кабиров. Англо русский словарь по электротехнике и электроэнергетике, Москва] Тематики электротехника, основные понятия EN ordered elimination technique … Справочник технического переводчика

метод упорядоченного исключения (при обработке матриц) — — [Я.Н.Лугинский, М.С.Фези Жилинская, Ю.С.Кабиров. Англо русский словарь по электротехнике и электроэнергетике, Москва] Тематики электротехника, основные понятия EN ordered elimination method … Справочник технического переводчика

метод стандартизации в статистике — метод исключения влияния неоднородности состава групп на результаты их сравнения путем расчета условных стандартизованных показателей … Большой медицинский словарь

МЕТОД — (лат. methōdus Российская энциклопедия по охране труда

Исключения — Обработка исключительных ситуаций (англ. exception handling) механизм языков программирования, предназначенный для описания реакции программы на ошибки времени выполнения и другие возможные проблемы (исключения), которые могут возникнуть при… … Википедия

Метод индукции — Индукция (лат. inductio наведение) процесс логического вывода на основе перехода от частного положения к общему. Индуктивное умозаключение связывает частные предпосылки с заключением не столько через законы логики, а скорее через некоторые… … Википедия

ИСКЛЮЧЕНИЯ ТЕОРИЯ — теория исключения неизвестных из системы алгебраич. уравнений. Более точно, пусть имеется система уравнений где fi многочлены с коэффициентами из заданного поля Р. Задача исключения неизвестных х 1 . х k из системы (1) (неоднородная задача… … Математическая энциклопедия

Метод Гаусса — У этого термина существуют и другие значения, см. Метод Гаусса (оптимизация). Метод Гаусса[1] классический метод решения системы линейных алгебраических уравнений (СЛАУ). Это метод последовательного исключения переменных, когда с помощью… … Википедия

МЕТОД ЭКСПЕРТНЫХ ОЦЕНОК — специфич. социологич. метод получения информации об объекте с помощью специалистов экспертов в определенной области. Экспертные оценки широко используются в прогнозировании, при определении целей соц. развития или принятии плановых решений,… … Российская социологическая энциклопедия

Источник

Как правильно использовать исключения

Исключения вместо if-ов: почему нет?

В большинстве случаев, мы читаем код чаще, чем пишем. Большинство практик программирования нацелены на упрощение понимания кода: чем проще код, тем меньше багов он содержит и тем проще его поддержка.

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

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

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

Исключения для валидации входящих данных

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

Очевидно, подобный подход имеет некоторые плюсы: он позволяет нам быстро «вернуться» из любого метода прямо в catch блок метода CreateEmployee.

Теперь давайте посмотрим на следующий пример:

Что общего имеют эти два сэмпла? Оба они позволяют прервать текущий поток выполнения и быстро перейти к определенной точке в коде. Единственная проблема в таком коде — он существенно ухудшает читаемость. Оба подхода затрудняют понимание кода, именно поэтому использование исключений для контроля потока выполнения программы часто уравнивают с использованием goto.

При использовании исключений сложно понять где именно они ловятся. Вы можете обернуть код, выбрасывающий исключение, в try/catch блок в том же методе, а можете поместить try/catch блок на несколько уровней по стеку выше. Вы никогда не можете знать наверняка сделано ли это намерено или нет:

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

Читайте также:  Производственный способ начисления амортизации формула

Есть ли способ лучше? Конечно:

Указание всех проверок явным образом делает ваши намерения намного более понятными. Эта версия метода проста и очевидна.

Исключения для исключительных ситуаций

Так когда же использовать исключения? Главная цель исключений — сюрприз! — обозначить исключительную ситуацию в приложении. Исключительная ситуация — это ситуация, в которой вы не знаете что делать и наилучшим выходом для вас является прекращение выполнения текущией операции (возможно, с предварительным логированием деталей ошибки).

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

Другим примером корректного использования исключений является валидация контрактов (code contract). Вы, как автор класса, ожидаете, что клиенты этого класса будут соблюдать его контракты. Ситуация, при которой контракт метода не соблюден, является исключительной и заслуживает выбрасывания исключения.

Как работать с исключениями, брошенными другими библиотеками?

Является ли ситуация исключительной, зависит от контекста. Разработчик сторонней библиотеки может не знать как иметь дело с проблемами подключения к БД, т.к. он не знает в каком контексте будет использоваться его библиотека.

В случае подобной проблемы, разработчик библиотеки не имеет возможности что-либо с ней сделать, поэтому бросание исключения будет подходящим решением. Вы можете взять Entity Framework или NHibernate в качестве примера: они ожидают, что БД всегда доступна и если это не так, бросают исключение.

С другой стороны, разработчик, использующий библиотеку, может ожидать, что база данных уходит в офлайн время от времени и разрабатывать свое приложение с учетом этого факта. В случае отказа БД, клиентское приложение может попытаться повторить ту же операцию или отобразить сообщение пользователю с предложением повторить операцию позже.

Таким образом, ситуация может быть исключительной с точки зрения нижележащего кода и ожидаемой с точки зрения клиентского кода. Как в этом случае работать с исключениями, брошенными такой библиотекой?

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

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

Метод SaveCustomer имеет понятную сигнатуру, говорящую нам, что в процессе сохранения клиента могут быть проблемы, что эти проблемы ожидаемы и что вы должны проверить возвращаемое значение чтобы убедиться, что все в порядке.

Стоит отметить широко известную практику, применимую в данном случае: не нужно оборачивать подобный код в generic обработчик. Generic обработчик утверждает, что любые исключения являются ожидаемыми, что по сути неправда.

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

Единственная ситуация, в которой generic обработчики применимы — это размещение их на самом высоком уровне по стеку приложения для отлова всех исключений, не пойманных кодом ниже, для того, чтобы залогировать их. Подобные исключения не следует пытаться обработать, все что можно сделать — это закрыть приложение (в случае со stateful приложением) или прекратить текущую операцию (в случае со stateless приложением).

Исключения и fail-fast принцип

Как часто вы встречаете код подобный этому?

Это пример некорректного использования generic обработчика исключений. Код выше подразумевает, что все исключения, приходящие из тела метода, являются признаком ошибки в процессе создания кастомера. В чем проблема подобного кода?

Помимо схожести с «goto» семантикой, обсуждаемой выше, пробема состоит в том, что исключение, приходящее в catch блок может не быть известным нам исключением. Исключение может быть как ArgumentException, которое мы ожидаем, так и ContractViolationException. В последнем случае, мы прячем баг, притворяясь, что нам известно как обрабатывать подобное исключение.

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

Наилучший способ работы с неожидаемыми исключениями — прекратить текущую операцию полностью и предотвратить распространение неконсистентного состояния по приложению.

Источник

Оцените статью
Разные способы