Кажется, существует распространенное заблуждение относительно использования Исключений в ООП, которое я хотел бы попытаться прояснить, и это восходит к тому, что я писал ранее о том, как мы думаем, когда исходим из процедурного подхода. мир (например, C или PHP примерно в 2001 году) и то, как мы должны мыслить в рамках объектно-ориентированной парадигмы.

В старомодном процедурном дизайне наиболее распространенным способом обработки ошибок является проверка возвращаемого значения функции. Например, функция может возвращать значение (например, целое число) в результате вычисления, если ошибки не возникает, но может возвращать нечто исключительное, например значение false или -1 когда что-то пошло не так. Это очень часто можно увидеть в таких языках, как C.

Одна из проблем этого подхода заключается в том, что мы получаем функции с несогласованным интерфейсом или сигнатурой. В PHP это обычно выглядит так:

/**
 * @return string|false
 */
public function process(string $value)
{
    if ($value === '') {
        return false;
    }
    
    // some something with string
    
    return $processedString
}

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

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

И исключение не ошибка. На самом деле, исключения в ООП лучше рассматривать как события. Как многие из вас, вероятно, знают, событие — это то, что прерывает нормальный ход программы, но отправляется явным образом. с помощью шины событий или диспетчера. В парадигме отправки событий (особенно асинхронной разновидности) рекомендуется просто "запустить и забыть" — обработчик или прослушиватель событий, который зарегистрирован для это конкретное событие поймает его и справится с ним.

Что ж, исключения, по сути, похожи друг на друга — это сигнал, который должен отправляться, когда что-то пошло не так с нашими вычислениями. Объектно-ориентированные языки имеют встроенный механизм для распространения событий вверх по уровням приложения почти так же, как шина событий передает событие своему обработчику или слушатель. Исключения, как и события, должны быть запущены и забыты. (Ну, не совсем забыто, как мы скоро увидим).

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

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

Давайте посмотрим на пример. Представьте, что у нас есть простой HTTP-клиент:

Как видите, этот клиент генерирует исключение HttpException, когда запрос curl приводит к ошибке. Теперь представьте, что класс, использующий этот клиент, представляет собой класс библиотечного каталога, которому необходимо получить список книг из удаленного API.

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

Теперь, если вы посмотрите на этот класс, представленный современной IDE, такой как PhpStorm, вы заметите, что IDE пытается привлечь ваше внимание к тому факту, что Client генерирует исключение, которое Каталог не обрабатывает:

IDE предлагает вам два способа решения этой проблемы:

  1. Окружите с помощью try/catch (и повторно создайте исключение с другим, относящимся к Каталогу).
  2. Добавьте тег @throws в аннотацию метода.

Если мы выберем 1-й вариант просто для того, чтобы преобразовать HttpException в CatalogListingException, мы вернемся к процедурной парадигме. Никто не заботится о том, что это раньше было HttpException, а теперь является CatalogListingException, если только это не была ошибка обработки, которая произошла внутрисамого класса Catalog.

Но так как эта ошибка не возниклавнутриКаталога,инаш Каталог ни в малейшей степени не заинтересован в исключение, выброшенное Клиентом — мы должны выбрать 2-й вариант.

Но «подождите», — скажете вы. Где отловить это исключение? Чтобы ответить на этот вопрос, нам нужно спросить: «Кого это волнует?». Другими словами, не все ли равно вышележащему слою (каркасу)? Возможно, ему нужно преобразовать это исключение в правильный ответ с ошибкой 500? В этом случае платформа должна его перехватить:

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

И теперь smart IDE сообщает нам, что в основном скрипте есть необработанное исключение:

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

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

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

В этом случае мы действительно делаем исключение по принципу «запускаем и забываем».

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