Разделяйте технические и логические исключения
(В оригинале - Distinguish Business Exceptions from Technical)
В большинстве случаев есть лишь две причины, из-за которых что-то может пойти не так во время выполнения программы. Это технические проблемы, мешающие работе приложения, и бизнес-логика, не дающая нам использовать приложение неправильно. В большинстве современных языков программирования, таких как LISP, Java, Smalltalk, и C#, для обеих ситуаций используется механизм исключений. Однако эти две ситуации столь различны, что их стоит аккуратно разделять и не смешивать. Если для обеих ситуаций использовать одну и ту же иерархию исключений (не говоря уже о том, чтобы использовать одни и те же классы), то это может стать источником серьезной путаницы.
Невосстановимая техническая проблема может произойти в случае ошибки в программе. Например, вы обращаетесь к 83-му элементу массива, в котором есть лишь 17 элементов. В этом случае программа ничего не может предпринять, чтобы это исправить, и должно возникнуть исключение. Или ваш код обращается к библиотеке, вызывая функцию с неправильными параметрами, вызывая описанную выше ситуацию внутри библиотеки.
Было бы ошибкой пытаться как-то решить описанную выше ситуацию. Вместо этого мы просто позволяем исключению «всплыть» на самый верх и позволить общему обработчику исключений сделать необходимую работу, чтобы сохранить систему в стабильном состоянии – откатить транзакции, записать сообщение в лог и выдать сообщение пользователю.
Один из вариантов этой ситуации – это когда вы находитетесь на стороне библиотеки, если вызывающий нарушил контракт вызова для вашей функции – передал какую-нибудь фигню в параметрах или не выполнил требуемых пред-условий. Это практически то же самое, что и обращение к 83-му элементу массива длиной в 17 элементов – проверку должна делать вызывающая сторона, если же этой проверки нет, проблема на вызывающей стороне. И правильная реакция – сгенерировать техническое исключение.
Немного другая, но все еще техническая проблема – это невозможность продолжать работать из-за проблем в среде окружения, например, отсутствие ответа от базы данных. В этом случае вы должны предполагать, что инфраструктура уже попыталась сделать все возможное, чтобы эту проблему решить, например, повторить попытку несколько раз, так и не получив результата. Но хоть причина и другая, для вашего кода ничего не поменялось – вы по-прежнему ничего не можете сделать для решения. Поэтому вы сообщаете о проблеме при помощи исключения, «всплывающего» наверх до общего обработчика.
В противоположность описанным выше ситуациям, бывает так, что продолжение работы программы невозможно из-за предметно-ориентированной проблемы. В этом случае мы получаем ситуацию, являющуюся исключением, т.е. необычную и нежелаемую, но не вызванную ошибкой в программе. Например, если попытаться снять со счета больше денег, чем там есть. Другими словами, эта ситуация – часть логики работы, и исключение – лишь альтернативный путь выполнения программы. Такое исключение должно корректно обрабатываться сразу же в точке вызова, непосредственно вызывающей стороной. И для таких ситуаций имеет смысл создать отдельную иерархию исключений.
Смешивание технических и логических исключений в общую иерархию размывает различия между ними и запутывает клиента по поводу контракта использования, необходимых условий перед вызовом и ситуаций, требующих реакции на них. Разделение этих двух концепций делает ситуацию более ясной и повышает шансы на то, что технические исключения будут обрабатываться общим фреймворком, а логические – непосредственно в клиентском приложении.
Автор оригинала - Dan Bergh Johnsson