?

Log in

No account? Create an account
All things considered... [entries|archive|friends|userinfo]
Qbit

[ userinfo | livejournal userinfo ]
[ archive | journal archive ]

Исключения [Aug. 23rd, 2010|05:47 am]
Qbit

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

Айзек Азимов

Давно собирался написать пост про исключения, их misusage и best practices — но всё никак не доходили руки (лето довольно насыщенное). А тут оказия — к инициализации нового проекта нужно составить гайдлайн для команды — официальный повод превозмочь прокрастинацию. Плюс доводилось недавно на эту тему пофлеймить на RSDN, так что буду ковать железо, пока свежо предание. Нижеизложенное кому-то покажется спорным, кому-то тривиальным, ну так на то и эпиграф, чтобы предостеречь. Комментарии в любом случае приветствуются.

Я делю исключения на такие группы (примерно так же они систематизированы в «FDG», гл. 7.2, стр. 221):

http://graph.gafol.net/cfDhbfMhV

Эта структура не обязательно должна быть отражена в иерархии классов стандартной библиотеки. Иерархия исключений в C++ ближе к указанной схеме, в .NET — дальше.

Приведённое деление принципиально, то есть существуют объективные различия в источниках этих исключительных ситуаций, их обработке и других параметрах (в тексте отмечены тегом <em>).

Прежде, чем приступить к рассмотрению разных типов исключений, оговорю некоторые соглашения. Речь буду вести в контексте разработки библиотеки (API для краткости) и использования этой библиотеки сторонними прикладными программистами.

Действующие лица:

  1. разработчик — автор API;
  2. разработчик — пользователь API;
  3. конечный пользователь приложения;
  4. администратор приложения (продвинутый пользователь);
  5. система.

Информация об ошибке передаётся:

  1. В типе исключения. Позволяет сепарировать исключения в момент ловли.
  2. В полях исключения. Позволяет восстанавливать контекст исключения, состояние при возникновении ошибки, например, фазу луны.
  3. В тексте сообщения. В случае usage-исключения содержит указание, как изменить код, чтобы избежать выброс исключения; ориентировано на пользователя API. В случае runtime-исключения содержит причину исключения в меру своей осведомлённости; ориентировано на пользователя API или конечного пользователя. В подавляющем большинстве случаев это сообщение нельзя отображать в UI — в нём либо содержатся избыточные детали, либо, наоборот, отсутствует контекст. Например, стандартные коллекции не знают, что работают со списком пользовательских аккаунтов, их сообщения по простоте душевной оперируют терминами «элемент коллекции».

Usage errors

Порождаются при невыполнении контрактов того или иного метода API со стороны вызывающего кода (т.е. источник ошибки — пользователь API). Скажем, метод библиотеки умеет работать только с непустыми строками, и поэтому бросает ArgumentException, если параметр String.IsNullOrWhitespace. Это не валидация аргументов; это проверка того, что правильность аргументов была проверена вызывающим кодом. Обработка: этот тип исключений не ловится в catch-блоке; реакция на них заключается в предварительной проверке в пользовательском коде выполнения предусловий перед вызовом метода библиотеки.

Krzysztof Cwalina: «A usage error represents an incorrectly written program and is something that can be avoided by changing the code that calls your API. There is no reason to programmaticaly handle usage errors; instead the calling code should be changed... In other words, usage errors can be fixed at compile-time, and the developer can ensure that they never occured in runtime.»

Jeffrey Richter: «It is very unusual to find code that catches any of these argument exceptions. When one of these exceptions is thrown, you almost always want the application to die. Then look at the exception stack trace, fix your source code so that you are passing the right argument, recompile the code, and retest.»

Информация об ошибке, как правило, полностью содержится в тексте сообщения, редко в данных экземпляра исключения или в его типе. Поэтому плодить иерархию исключений смысла нет, лучше использовать стандартные. Сообщение ни в каком виде не должно передаваться конечному пользователю (message box, log, etc). Адресатом сообщения является пользователь API, ему должно быть понятно, кто виноват, и что делать. Т.е. буквально, сообщение может состоять из двух предложений: 1) что произошло, 2) как исправить вызывающий код, чтобы избежать выброс исключения. Текст должен быть на английском, локализация не обязательна (имхо).

Logic errors

Ассерты — это утверждения, которые автор API делает относительно логики своей библиотеки. Например, постулирует инвариант: «Мамой клянусь, в этом состоянии эта переменная должна быть чётной по построению!» Logic exceptions — это исключения, выбрасываемые в результате провала ассертов. Бросая подобное исключение, автор как бы собственноручно расписывается, что в результате своей ошибки кодирования не смог достичь предполагаемых результатов. Т.е. источником ошибки является автор API.

Выброс подобных исключений часто оборачивается в вызов вспомогательных функций типа Assert(condition). Используется, по сути, как документирующий комментарий, проверяемый в рантайме. Часто эти функции вызывают не по делу или злоупотребляют именем Assert (вместо Check или Validate). Скажем, нельзя писать Assert(inputParam != null) для проверки предусловий, потому что это случай usage-ошибки, и обработка должна быть соответствующая. Точно так же нельзя писать Assert(File.Exists(...)), потому что отсутствие файла — это рассматриваемая ниже runtime-ошибка, к ошибке в логике не имеет отношения.

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

Этот тип исключения не должен обрабатываться программно в catch-блоке; реакция заключается в баг-репорте автору библиотеки. Как и в предыдущем пункте, информация об ошибке передаётся не столько в типе или данных, сколько в тексте сообщения. Выводить конечному пользователю текст не предполагается, адресаты сообщения — разработчики, автор и пользователь API. В идеале — они совпадают, т.е. логические ошибки отсеиваются при тестировании. В худшем случае, последний получает сообщение от первого посредством баг-репорта. Текст сообщения должен быть понятен раработчику, в идеале — любому, кто будет поддерживать код библиотеки, поэтому язык сообщения должен быть нейтральным (английским), локализация не обязательна.

Program errors

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

Пользователь API может исключения:

  1. пропустить (let it propagate);
  2. поймать и:
    • проборосить (throw;, но не throw ex;, чтобы не оборвать stack trace);
    • обернуть (вложить старое исключение в InnerException нового);
    • заменить (иногда из соображений security исключение не должно пересекать границу слоя);
    • отменить (если на текущем уровне достаточно средств для исчерпывающей обработки исключительной ситуации).

Пример стратегии обработки исключений в приложении может выглядеть, скажем, так:

Exception Handling Application Block

Здесь, поднимаясь от слоя DAL к слою BL, исключение логируется и, преобразуя свой тип, выражается в терминах бизнес-логики. Внутри слоя BL оно только логируется и пробрасывается не меняясь. Пересекая границу с PL, вся лишняя (возможно, критичная по отношению к безопасности) информация отсекается. Внутри PL уже есть возможность оповестить конечного пользователя.

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

Чтобы избежать многократного дублирования catch-блоков для стандарнтых действий типа «Log&Wrap» или «Notify User», можно использовать The Exception Handling Application Block. Один из сценариев его использования — задать политики декларативно в конфиге, разрезолвить экземпляр из IoC-контейнера и вызывать exceptionManager.Process(() => ...) для методов, исключения которых мы собирались перехватить.

System Failures

Этот пункт я решил оставить за кадром. Одно могу сказать, при OutOfMemoryException поздно пить боржоми, возможности обработать исключение может и не быть. Часто единственной реакцией является Environment.FailFast(message).
LinkReply

Comments:
[User Picture]From: jakobz
2010-08-23 09:05 am (UTC)
А ведь и правда, оно на поверхности, но почему-то я нигде не видел и сам не дошел до такой классификации. Отличная мысль про разделение ответственности между автором/пользователем API и причинах ошибок (внешние/внутренние).
(Reply) (Thread)
[User Picture]From: pavel_valerich
2010-08-23 11:43 am (UTC)
хорошая классификация
(Reply) (Thread)
[User Picture]From: 109
2010-08-23 11:58 pm (UTC)
отличный пост! только я бы вместо "logic errors" назвал бы как-то типа "invariant violations".
(Reply) (Thread)
[User Picture]From: wizzard0
2010-08-24 11:56 am (UTC)
Invariant violation - симптом. Logic error - причина. Поэтому, имхо, так правильнее.
(Reply) (Parent) (Thread)
[User Picture]From: 109
2010-08-24 06:18 pm (UTC)
не, не согласен. все остальные типы ошибок тогда тоже симптомы.
(Reply) (Parent) (Thread)
[User Picture]From: bik_top
2010-08-29 06:24 pm (UTC)

Code Contracts

Относительно того, что в посте названо «логические ошибки», можно почитать в свежей статье на RSDN: http://rsdn.ru/article/design/Code_Contracts.xml.

Edited at 2010-08-29 06:26 pm (UTC)
(Reply) (Thread)
[User Picture]From: mrjazz
2010-08-30 09:03 pm (UTC)
Отличный подход, только сегодня имел диспут о преимуществах-недостатках исключений перед логгированием. Все-таки с ексепшенами, красивее получается.
(Reply) (Thread)