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

11

Я борюсь с очень простым вопросом:

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

Я думаю о следующей стратегии:

1) Что не так?

  • Что-то спрашивают, что не разрешено.
  • Что-то спрашивают, это разрешено, но не работает из-за неправильных параметров.
  • Что-то спрашивают, это разрешено, но не работает из-за внутренних ошибок.

2) Кто запускает запрос?

  • Клиентское приложение
  • Другое серверное приложение

3) Передача сообщений: поскольку мы имеем дело с серверным приложением, все дело в получении и отправке сообщений. Так что, если отправка сообщения идет не так?

Таким образом, мы можем получить следующие типы исключений:

  • ServerNotAllowedException
  • ClientNotAllowedException
  • ServerParameterException
  • ClientParameterException
  • InternalException (в случае, если сервер не знает, откуда поступает запрос)
    • ServerInternalException
    • ClientInternalException
  • MessageHandlingException

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

заранее спасибо

Dominique
источник
5
Вы не упомянули, чего хотите достичь с помощью иерархии классов исключений (и это совсем не очевидно). Значимое ведение журнала? Разрешить клиентам разумно реагировать на различные исключения? Или что?
Ральф Клеберхофф
2
Вероятно, полезно сначала проработать некоторые истории и варианты использования и посмотреть, что получится. Например: клиент запрашивает X , не разрешено. Клиент запрашивает X , но запрос недействителен. Поработайте над ними, думая о том, кто должен обрабатывать исключение, что они могут с ним сделать (подсказка, повтор, все что угодно) и какая информация им нужна, чтобы сделать это хорошо. Затем , как только вы узнаете, каковы ваши конкретные исключения и какая информация нужна обработчикам для их обработки, вы можете сформировать их в хорошую иерархию.
бесполезно
1
Уместно предположить: softwareengineering.stackexchange.com/questions/278949/…
Мартин Ба,
1
Я никогда не понимал желание использовать так много различных типов исключений. Как правило, для большинства catchблоков, которые я использую, я не использую намного больше исключений, чем какое сообщение об ошибке оно содержит. У меня нет ничего особенного, что я могу сделать для исключения, связанного с невозможностью прочитать файл как файл, который не выделяет память во время процесса чтения, поэтому я склонен просто ловить std::exceptionи сообщать о сообщении об ошибке, которое в нем содержится, возможно, украшающем это с "Failed to open file: %s", ex.what()буфером стека перед печатью.
3
Кроме того, я не могу предвидеть все типы исключений, которые будут выброшены в первую очередь. Я мог бы предвидеть их сейчас, но коллеги могут вводить новые в будущем, например. Поэтому для меня безнадежно знать, на данный момент и навсегда, все различные типы исключений, которые могут быть сгенерированы в процессе операция. Так что я просто ловлю супер, как правило, с помощью одного блока улова. Я видел примеры людей, использующих много разных catchблоков на одном сайте восстановления, но часто это просто игнорирование сообщения внутри исключения и печать более локализованного сообщения ...

Ответы:

5

Основные пометки

(немного необъективно)

Обычно я бы не стал использовать подробную иерархию исключений.

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

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

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

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

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

Ваша иерархия

Кто запускает запрос? Я не думаю, что вам понадобится информация о том, кто запускает запрос. Я даже не представляю, как вы знаете это глубоко внутри некоторого стека вызовов.

Обработка сообщений : это не другой аспект, а просто дополнительные случаи для «Что идет не так?».

В комментарии вы говорите о флаге « no logging » при создании исключения. Я не думаю, что в том месте, где вы создаете и выбрасываете исключение, вы можете принять надежное решение, регистрировать это исключение или нет.

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

Ральф Клеберхофф
источник
По моему опыту, код ошибки в сочетании с удобным для пользователя текстом работает очень хорошо. Администраторы могут использовать код ошибки, чтобы найти дополнительную информацию.
Сьорд
1
Мне нравится идея этого ответа в целом. Из моего опыта Исключения действительно никогда не должны превращаться в слишком сложного зверя. Основная цель исключения - разрешить вызывающему коду решить конкретную проблему и получить соответствующую информацию об отладке / повторных попытках, не вмешиваясь в реакцию функции.
greggle138
2

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

  • Если ваш язык или структура уже предоставляют общие классы исключений, используйте их там, где они уместны и где их разумно ожидать . Не определяйте свои собственные ArgumentNullExceptionили ArgumentOutOfRangeисключительные классы. Абоненты не будут ожидать, чтобы поймать их.

  • Определите MyClientServerAppExceptionбазовый класс для охвата ошибок, которые являются уникальными в контексте вашего приложения. Никогда не бросайте экземпляр базового класса. Неоднозначная реакция на ошибку - это ХУМАЯ ВЕЩЬ . Если есть «внутренняя ошибка», объясните, что это за ошибка.

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

  • Не пытайтесь определить все ваши классы исключений до того, как они будут фактически использованы. Вы закончите заново делать большую часть этого. Когда вы сталкиваетесь с ошибкой во время написания кода, решите, как лучше всего описать эту ошибку. В идеале это должно выражаться в контексте того, что пытается сделать вызывающий абонент.

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

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

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

Марк Беннингфилд
источник
2
За исключением вашего второго до последнего пункта (о стоимости исключений), ответ хороший. Этот пункт о стоимости исключений вводит в заблуждение, потому что во всех распространенных способах реализации исключений на низком уровне стоимость создания исключения полностью не зависит от глубины стека вызовов. Альтернативные методы сообщения об ошибках могут быть лучше, если вы знаете, что ошибка будет обработана непосредственным абонентом, но не для того, чтобы убрать несколько функций из стека вызовов, прежде чем вызвать исключение.
Барт ван Инген Шенау
@BartvanIngenSchenau: я старался не привязывать это к определенному языку. В некоторых языках ( например , Java ) глубина стека вызовов влияет на стоимость реализации. Я отредактирую, чтобы отразить, что это не так вырезано и высушено.
Марк Беннингфилд
0

Ну, я бы порекомендовал вам в первую очередь создать базовый Exceptionкласс для всех проверенных исключений, которые могут быть выданы вашим приложением. Если ваше приложение было вызвано DuckType, то создайте DuckTypeExceptionбазовый класс.

Это позволяет вам перехватить любое исключение из вашего базового DuckTypeExceptionкласса для обработки. Отсюда ваши исключения должны иметь описательные имена, которые могут лучше выделить тип проблемы. Например, «DatabaseConnectionException».

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

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

Мое личное предпочтение состоит в том, чтобы не выбрасывать непроверенное RuntimeExceptionкак еще одно исключение, поскольку по природе непроверенного исключения вы этого не ожидаете, и поэтому отбрасывание его под другим исключением скрывает информацию. Однако, если это ваше предпочтение, вы все равно можете поймать RuntimeExceptionи бросить a, DuckTypeInternalExceptionчто в отличие от DuckTypeExceptionпроисходит RuntimeExceptionи поэтому не проверяется.

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

Как правило, ваши ловушки try должны становиться все более универсальными, когда вы перемещаетесь вверх по стеку вызовов вызывающих абонентов для обработки исключений, и, опять же, в вашем главном контроллере вы бы не обрабатывали, DatabaseConnectionExceptionа скорее просто простое, DuckTypeExceptionиз которого выводятся все ваши проверенные исключения.

Нил
источник
2
Обратите внимание, что вопрос помечен как "C ++".
Мартин Ба,
0

Попробуйте упростить это.

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

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

Для примера, посмотрите этот код Java:

public class SystemErrorCode implements ErrorCode {

    INVALID_NAME(101),
    ORDER_NOT_FOUND(102),
    PARAMETER_NOT_FOUND(103),
    VALUE_TOO_SHORT(104);

    private final int number;

    private ErrorCode(int number) {
        this.number = number;
    }

    @Override
    public int getNumber() {
        return number;
    }
}

И ваше уникальное исключение:

public class SystemException extends RuntimeException {

    private ErrorCode errorCode;

    public SystemException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

}

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

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

Dherik
источник
2
Обратите внимание, что вопрос помечен как "C ++".
Сьорд
Я отредактировал, чтобы адаптировать идею.
Дерик
0

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

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

Исключения должны быть в контексте вызываемой функции. Например: функция, которая решает квадратные уравнения . Он должен обрабатывать два исключения: Division_by_zero и square_root_of_negative_number. Но эти исключения не имеют смысла для функций, вызывающих этот решатель квадратного уравнения. Они происходят из-за метода, используемого для решения уравнений, и простое их отбрасывание обнажает внутренние органы и нарушает инкапсуляцию. Вместо этого Division_by_zero должен быть переброшен как not_quadratic и square_root_of_negative_number и no_real_roots.

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

shawnhcorey
источник