Исключения: зачем бросать рано? Зачем ловить поздно?

156

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

Зачем мне бросать рано и ловить поздно, если на низкоуровневом уровне выдается исключение нулевого указателя? Почему я должен ловить это на более высоком уровне? Мне не имеет смысла отлавливать низкоуровневое исключение на более высоком уровне, например на бизнес-уровне. Кажется, это нарушает заботы каждого слоя.

Представьте себе следующую ситуацию:

У меня есть сервис, который рассчитывает фигуру. Чтобы вычислить показатель, сервис обращается к репозиторию, чтобы получить необработанные данные, и к некоторым другим сервисам, чтобы подготовить расчет. Если что-то пошло не так на уровне извлечения данных, почему я должен выбросить DataRetrievalException на более высокий уровень? В отличие от этого, я бы предпочел заключить исключение в значимое исключение, например, в CalculationServiceException.

Зачем бросать рано, зачем ловить поздно?

shylynx
источник
104
Идея «поймать поздно» заключается в том, чтобы поймать как можно раньше, а не поймать как можно раньше. Например, если у вас есть анализатор файлов, нет смысла обрабатывать файл, который не найден. Что вы собираетесь делать с этим, каков ваш путь восстановления? Там нет ни одного, так что не лови. Перейдите к своему лексеру, что вы там делаете, как вы восстанавливаетесь после этого, чтобы ваша программа могла продолжаться? Не может, пусть проходит исключение. Как ваш сканер может справиться с этим? Не может, пусть это пройдет. Как вызывающий код может справиться с этим? Он может либо попробовать другой путь к файлу, либо предупредить пользователя, так что ловите.
Фоши
16
Есть очень немного случаев, когда NullPointerException (я предполагаю, что это означает, что NPE) когда-либо должен быть пойман; если возможно, этого следует избегать в первую очередь. Если у вас есть NullPointerExceptions, то у вас есть неработающий код, который необходимо исправить. Скорее всего, это тоже легко исправить.
Фил
6
Пожалуйста, ребята, прежде чем предлагать закрыть этот вопрос как дубликат, убедитесь, что ответ на другой вопрос не очень хорошо отвечает на этот вопрос.
Док Браун
1
(Citebot) today.java.net/article/2003/11/20/… Если это не происхождение цитаты, укажите ссылку на источник, который, по вашему мнению, является наиболее вероятной оригинальной цитатой.
rwong
1
Просто напоминание для тех, кто достиг этого вопроса и занимается разработкой для Android. В Android исключения должны перехватываться и обрабатываться локально - в той же функции, в которой они были впервые обнаружены. Это потому, что исключения не распространяются на обработчики сообщений - ваше приложение будет уничтожено, если это произойдет. Таким образом, вы не должны ссылаться на этот совет при разработке Android.
Rwong

Ответы:

118

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

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

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

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

Поймать → Ретроу

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

Поймать → Ручка

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

Поймать → Ошибка возврата

Несмотря на то, что существуют ситуации, когда это уместно, перехват исключений и возвращение вызывающей стороне значения ошибки следует рассмотреть для рефакторинга в реализацию Catch → Rethrow.

Майкл Шоу
источник
Да, я уже знаю, и не подлежит сомнению, что я должен выдавать исключения в том месте, где возникает ошибка. Но почему я не должен поймать NPE и вместо этого позволить ему взобраться на Stacktrace? Я всегда ловил NPE и превращал его в значимое исключение. Я также не вижу никакого преимущества, почему я должен выдавать DAO-Exception на уровне сервиса или пользовательского интерфейса. Я всегда ловил его на уровне службы и включал в исключение службы с дополнительной подробной информацией о том, почему вызов службы не удался.
shylynx
8
@shylynx Поймать исключение и затем выбросить более значимое исключение - это хорошая вещь. Чего не следует делать, так это слишком рано поймать исключение и затем не выбрасывать его. Ошибка, о которой говорится в предупреждении, заключается в слишком раннем перехвате исключения, а затем в попытке обработать его на неправильном уровне кода.
Симон Б
Обеспечение очевидности контекста в момент получения исключения облегчает жизнь разработчикам в вашей команде. NPE требует дальнейшего изучения, чтобы понять проблему
Майкл Шоу
4
@shylynx Кто-то может задать вопрос: «Почему в вашем коде есть точка, которая может выбросить NullPointerException? Почему бы не проверить nullи не сгенерировать исключение (может быть IllegalArgumentException) раньше, чтобы вызывающий точно знал, куда nullпоступило плохое ?» Я полагаю, что это было бы то, что предложила бы часть «бросить рано».
jpmc26
2
@jpmc Я взял NPE только в качестве примера, чтобы подчеркнуть озабоченность вокруг слоев и исключений. Я мог бы также заменить его на IllegalArgumentException.
Shylynx
56

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

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

Упаковка исключений в правильные типы является чисто ортогональной задачей.

Doval
источник
1
+1 за четкое объяснение, почему разные уровни имеют значение. Отличный пример ошибки файловой системы.
Хуан Карлос Кото
24

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

ТАК ПОЧЕМУ ИСКЛЮЧЕНИЯ?

Похоже, что возникла путаница, почему исключения существуют в первую очередь. Позвольте мне поделиться здесь большим секретом: причина исключений и их обработка ... АБСТРАКЦИЯ .

Вы видели такой код:

static int divide(int dividend, int divisor) throws DivideByZeroException {
    if (divisor == 0)
        throw new DivideByZeroException(); // that's a checked exception indeed

    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    try {
        int res = divide(a, b);
        System.out.println(res);
    } catch (DivideByZeroException e) {
        // checked exception... I'm forced to handle it!
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Это не то, как исключения должны быть использованы. Код, подобный приведенному выше, существует в реальной жизни, но он скорее аберрация и действительно исключение (каламбур). Например, определение деления , даже в чистой математике, является условным: всегда «код вызывающей стороны» должен обрабатывать исключительный случай нуля, чтобы ограничить входную область. Это ужасно. Это всегда боль для звонящего. Тем не менее, для таких ситуаций естественным путем является паттерн check-then-do :

static int divide(int dividend, int divisor) {
    // throws unchecked ArithmeticException for 0 divisor
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt();
    if (b != 0) {
        int res = divide(a, b);
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

В качестве альтернативы вы можете использовать полную команду в стиле ООП, например:

static class Division {
    final int dividend;
    final int divisor;

    private Division(int dividend, int divisor) {
        this.dividend = dividend;
        this.divisor = divisor;
    }

    public boolean check() {
        return divisor != 0;
    }

    public int eval() {
        return dividend / divisor;
    }

    public static Division with(int dividend, int divisor) {
        return new Division(dividend, divisor);
    }
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Division d = Division.with(a, b);
    if (d.check()) {
        int res = d.eval();
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Как видите, код вызывающей стороны несет бремя предварительной проверки, но не выполняет обработки исключений после. Если ArithmeticExceptionкогда-либо приходит от вызова divideили eval, то именно ВЫ должны обрабатывать исключения и исправлять ваш код, потому что вы забыли check(). По тем же причинам поймать NullPointerExceptionпочти всегда неправильно.

Теперь есть некоторые люди , которые говорят , что они хотят видеть исключительные случаи в / метод сигнатуры функции, то есть явно продлить выходной домен . Именно они предпочитают проверенные исключения . Конечно, изменение домена вывода должно заставить любой код прямого вызова адаптироваться, и это действительно будет достигнуто с проверенными исключениями. Но вам не нужны исключения для этого! Именно поэтому у вас есть Nullable<T> общие классы , классы регистра , алгебраические типы данных и типы союзов . Некоторые OO люди могут даже предпочесть возвращаться null для простых ошибок, таких как это:

static Integer divide(int dividend, int divisor) {
    if (divisor == 0) return null;
    return dividend / divisor;
}

static void doDivide() {
    int a = readInt();
    int b = readInt(); 
    Integer res = divide(a, b);
    if (res != null) {
        System.out.println(res);
    } else {
        System.out.println("Nah, can't divide by zero. Try again.");
    }
}

Технически исключения могут быть использованы для целей, подобных описанным выше, но здесь есть смысл: исключений для такого использования не существует . Исключения составляют про абстракция. Исключение составляют косвенные указания. Исключения позволяют расширять «конечный» домен, не нарушая прямых клиентских контрактов и откладывая обработку ошибок до «где-то еще». Если ваш код генерирует исключения, которые обрабатываются в прямых вызывающих программах одного и того же кода, без каких-либо промежуточных уровней абстракции, то вы делаете это НЕПРАВИЛЬНО

КАК ПОЗДАТЬ ПОЗДНО?

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

Этот вариант использования: Программирование против абстракций ресурсов ...

Да, бизнес-логика должна быть запрограммирована против абстракций , а не конкретных реализаций. Код «разводки» IOC верхнего уровня создаст конкретные реализации абстракций ресурса и передаст их бизнес-логике. Здесь нет ничего нового. Но конкретные реализации этих абстракций ресурсов могут потенциально генерировать свои собственные специфические для реализации исключения , не так ли?

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

«Ага!», Вы можете сказать: «Но именно поэтому мы можем создавать подклассы исключений и создавать иерархии исключений» (см. Мистер Спринг !). Позвольте мне сказать вам, что это ошибка. Во-первых, в каждой разумной книге об ООП говорится, что конкретное наследование является плохим, однако этот основной компонент JVM, обработка исключений, тесно связан с конкретным наследованием. По иронии судьбы, Джошуа Блох, возможно, не написал бы свою книгу «Эффективное Java» прежде, чем он смог бы получить опыт работы с рабочей JVM, не так ли? Это скорее книга «извлеченных уроков» для следующего поколения. Во-вторых, и что еще более важно, если вы поймали исключение высокого уровня, то как вы собираетесь его обрабатывать?PatientNeedsImmediateAttentionException: мы должны дать ей таблетку или ампутировать ее ноги !? Как насчет оператора switch для всех возможных подклассов? Там идет ваш полиморфизм, там идет абстракция. Вы поняли.

Так, кто может обращаться с исключениями конкретного ресурса? Это должен быть тот, кто знает конкременты! Тот, кто создал ресурс! Код "проводка" конечно! Проверь это:

Бизнес-логика закодирована против абстракций ... НЕТ ОБРАБОТКИ БЕТОННЫХ РЕСУРСОВ!

static interface InputResource {
    String fetchData();
}

static interface OutputResource {
    void writeData(String data);
}

static void doMyBusiness(InputResource in, OutputResource out, int times) {
    for (int i = 0; i < times; i++) {
        System.out.println("fetching data");
        String data = in.fetchData();
        System.out.println("outputting data");
        out.writeData(data);
    }
}

Между тем где-то еще конкретные реализации ...

static class ConstantInputResource implements InputResource {
    @Override
    public String fetchData() {
        return "Hello World!";
    }
}

static class FailingInputResourceException extends RuntimeException {
    public FailingInputResourceException(String message) {
        super(message);
    }
}

static class FailingInputResource implements InputResource {
    @Override
    public String fetchData() {
        throw new FailingInputResourceException("I am a complete failure!");
    }
}

static class StandardOutputResource implements OutputResource {
    @Override
    public void writeData(String data) {
        System.out.println("DATA: " + data);
    }
}

И, наконец, код подключения ... Кто обрабатывает конкретные исключения ресурсов? Тот, кто знает о них!

static void start() {
    InputResource in1 = new FailingInputResource();
    InputResource in2 = new ConstantInputResource();
    OutputResource out = new StandardOutputResource();

    try {
        ReusableBusinessLogicClass.doMyBusiness(in1, out, 3);
    }
    catch (FailingInputResourceException e)
    {
        System.out.println(e.getMessage());
        System.out.println("retrying...");
        ReusableBusinessLogicClass.doMyBusiness(in2, out, 3);
    }
}

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

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

СУММА СУММАРУМ

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

НТН. Удачного кодирования!

Даниэль Динниес
источник
Вы правы в том, что код, предоставляющий интерфейс, будет знать больше о том, что может пойти не так, как код, использующий интерфейс, но предположите, что метод использует два ресурса одного и того же типа интерфейса, и сбои нужно обрабатывать по-разному? Или, если один из этих ресурсов внутри, как деталь реализации, не известная его создателю, использует другие вложенные ресурсы того же типа? Наличие бизнес-уровня WrappedFirstResourceExceptionили WrappedSecondResourceExceptionтребование "проводного" уровня, чтобы заглянуть внутрь этого исключения, чтобы увидеть основную причину проблемы ...
суперкат
... может быть неприлично, но это может показаться лучше, чем предполагать, что любое FailingInputResourceисключение будет результатом операции с in1. На самом деле, я думаю, что во многих случаях правильный подход состоит в том, чтобы уровень проводки передавал объект обработки исключений, и чтобы бизнес-уровень включал объект, catchкоторый затем вызывает handleExceptionметод этого объекта . Этот метод может перебрасывать или предоставлять данные по умолчанию, или выдавать подсказку «Abort / Retry / Fail» и позволить оператору решать, что делать и т. Д. В зависимости от того, что требуется приложению.
суперкат
@supercat Я понимаю, что вы говорите. Я бы сказал, что конкретная реализация ресурса ответственна за знание исключений, которые она может выдать. Он не должен указывать все (есть так называемое неопределенное поведение ), но он должен гарантировать отсутствие двусмысленности. Кроме того, непроверенные исключения времени выполнения должны быть задокументированы. Если это противоречит документации, то это ошибка. Если код вызывающей стороны должен делать что-то разумное в отношении исключения, то минимум заключается в том, что ресурс оборачивает их в некоторые UnrecoverableInternalException, как код ошибки HTTP 500.
Даниэль Динниес
@supercat О вашем предложении по настраиваемым обработчикам ошибок: точно! В моем последнем примере логика обработки ошибок жестко запрограммирована, вызывая статический doMyBusinessметод. Это было сделано для краткости, и вполне возможно сделать его более динамичным. Такой Handlerкласс может быть создан с некоторыми ресурсами ввода / вывода и иметь handleметод, который получает класс, реализующий ReusableBusinessLogicInterface. Затем вы можете объединить / настроить для использования различных реализаций обработчика, ресурсов и бизнес-логики на уровне проводки где-то над ними.
Даниэль Динниес
10

Чтобы правильно ответить на этот вопрос, давайте сделаем шаг назад и зададим еще более фундаментальный вопрос.

Почему у нас есть исключения в первую очередь?

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

Давайте посмотрим на некоторый код:

double MethodA()
{
    return PropertyA - PropertyB.NestedProperty;
}

Этот код, очевидно, может генерировать исключение нулевой ссылки, если PropertyBравно null. В этом случае мы могли бы сделать две вещи, чтобы «исправить» ситуацию. Мы могли бы:

  • Автоматически создавать PropertyB, если у нас его нет; или же
  • Пусть исключение всплывает до вызывающего метода.

Создание PropertyB здесь может быть очень опасным. По какой причине этот метод должен создавать PropertyB? Конечно, это нарушит принцип единственной ответственности. По всей вероятности, если PropertyB здесь не существует, это означает, что что-то пошло не так. Метод вызывается для частично сконструированного объекта, или PropertyB был установлен равным нулю. Создавая здесь PropertyB, мы могли бы скрывать гораздо более серьезную ошибку, которая может укусить нас позже, такую ​​как ошибка, приводящая к повреждению данных.

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

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

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

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

Стивен
источник
Я думаю, что вы используете вышестоящих разработчиков в неправильном смысле. Кроме того, вы сказали, что это нарушает принцип единой ответственности, но на самом деле многие инициализация по требованию и кэширование значений реализованы таким образом (с должным контролем параллелизма вместо конечно)
Даниэль Динниес
В вашем данном примере, что о проверке нуля перед операцией вычитания, как этотif(PropertyB == null) return 0;
user1451111
1
Можете ли вы также уточнить свой последний абзац, в частности, что вы подразумеваете под « уровнем абстракции ».
user1451111
Если мы выполняем некоторую работу ввода-вывода, уровень абстракции для отлова исключения ввода-вывода был бы тем, где мы выполняем эту работу. В этот момент у нас есть вся информация, необходимая нам, чтобы решить, хотим ли мы повторить попытку или вызвать окно сообщения для пользователя или создать объект, используя набор значений по умолчанию.
Стивен
«В данном примере, как насчет проверки на нулевое значение перед операцией вычитания, например, если (PropertyB == null) возвращает 0;» Тьфу. Это будет говорить вызывающему методу, что у меня есть действительные вещи, которые нужно вычесть и из. Конечно, это контекстно, но в большинстве случаев было бы плохой практикой проводить здесь проверку ошибок.
Стивен
6

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

Поймайте, как только вы узнаете, что нужно сделать, чтобы исправить ошибку (обычно это не то, куда вы добавляете, иначе вы можете просто использовать if-else), если был передан неверный параметр, то слой, предоставивший параметр, должен иметь дело с последствиями ,

чокнутый урод
источник
1
Вы писали: Бросай скорее, ... Поймай скорее ...! Почему? Это совершенно противоположный подход в отличие от «бросай рано, лови поздно».
shylynx
1
@shylynx Я не знаю, откуда взялся «бросай рано, лови поздно», но его ценность сомнительна. Что именно означает «поздно»? Где имеет смысл поймать исключение (если оно вообще есть), зависит от проблемы. Единственное, что ясно, это то, что вы хотите обнаруживать проблемы (и бросать) как можно раньше.
Довал
2
Я предполагаю, что «поймать поздно» предназначено для того, чтобы противопоставить практику ловли, прежде чем вы сможете узнать, что делать, чтобы исправить ошибку - например, иногда вы видите функции, которые ловят все просто, чтобы они могли напечатать сообщение об ошибке и затем сбросить исключение.
@Hurkyl: Проблема с «ловлей поздно» заключается в том, что если исключение всплывает через слои, которые ничего о нем не знают, то для кода, который может быть в состоянии что-то сделать с ситуацией, может быть трудно понять, что вещи действительно таковы. ожидается. В качестве простого примера, предположим, что если анализатору для файла пользовательского документа необходимо загрузить КОДЕК с диска, и при чтении возникает ошибка диска, код, вызывающий синтаксический анализатор, может действовать неправильно, если он считает, что при чтении пользователя произошла ошибка диска документ.
суперкат
4

Действующее бизнес-правило: «Если программное обеспечение более низкого уровня не может рассчитать значение, тогда ...»

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

Сору
источник
2

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

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

Если при сборе необработанных данных на уровне данных произошла ошибка, создайте исключение, чтобы уведомить того, кто запросил данные. Не пытайтесь обойти эту проблему здесь. Сложность обработки кода может быть очень высокой. Также уровень данных отвечает только за запрос данных, а не за обработку ошибок, возникающих при этом. Это то, что означает «бросить рано» .

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

Ошибка службы может быть отслежена уровнем, вызывающим службу (например, UI). И это то, что означает «поймать поздно» . Если вы не можете обработать исключение на нижнем уровне, сбросьте его. Если самый верхний слой не может обработать исключение, обработайте его! Это может включать ведение журнала или его представление.

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

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

Aschratt
источник
1

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

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

davidk01
источник