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

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

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

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

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

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

142 голоса | спросил shylynx 3 MarpmMon, 03 Mar 2014 16:09:37 +04002014-03-03T16:09:37+04:0004 2014, 16:09:37

8 ответов


106

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

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

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

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

Catch â † 'Rethrow

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

Catch â † 'Handle

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

Catch â † 'Ошибка возврата

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

ответил Michael Shaw 3 MarpmMon, 03 Mar 2014 16:19:59 +04002014-03-03T16:19:59+04:0004 2014, 16:19:59
51

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

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

Завершение исключений в правильных типах является чисто ортогональным.

ответил Doval 3 MarpmMon, 03 Mar 2014 16:46:39 +04002014-03-03T16:46:39+04:0004 2014, 16:46:39
21

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

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

Кажется, существует довольно путаница в том, почему в первую очередь существуют исключения. Позвольте мне поделиться большим секретом здесь: причина исключений и обработка исключений - это ... ABSTRACTION .

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

 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.");
    }
}

Это не то, как следует использовать исключения. Код, подобный приведенному выше, существует в реальной жизни, но они скорее аберрации, а действительно являются исключением (каламбур). Определение division , например, даже в чистой математике, является условным: он всегда является «кодом вызывающего», которому приходится обрабатывать исключительный случай нуля, чтобы ограничить входной домен. Это ужасно. Это всегда боль для вызывающего. Тем не менее, для таких ситуаций шаблон 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, то он YOU , который должен выполнять обработку исключений и исправить свой код, потому что вы забыли код check(). По аналогичным причинам выловив NullPointerException почти всегда не так.

Теперь есть люди, которые говорят, что хотят видеть исключительные случаи в сигнатуре метода /функции, т. е. явно расширять вывод домен . Это те, кто предпочитает проверенные исключения . Конечно, изменение выходного домена должно заставить любой прямой код вызывающего абонента адаптироваться, и это действительно будет достигнуто с проверенными исключениями. Но для этого вам не нужны исключения! Вот почему у вас есть Nullable<T> общие классы , case classes , алгебраические типы данных и типы соединений . Некоторые люди 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.");
    }
}

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

КАК ПОСЕТИТЬ ПОЗЖЕ?

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

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

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

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

«Ага!», вы можете сказать: «но именно поэтому мы можем использовать исключения для подкласса и создавать иерархии исключений» (проверьте Mr. Spring !). Позвольте мне сказать вам, это ошибка. Во-первых, каждая разумная книга по ООП говорит о том, что конкретное наследование плохое, но как-то этот основной компонент 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, и вам нужны автоматические повторные попытки и повторная инициализация ресурсов сеанса или запроса и т. Д. Логике проводки на объектах нижнего уровня могут быть предоставлены абстрактные заводы для создавать ресурсы, поэтому не осознавая точных реализаций. Только области более высокого уровня действительно будут знать, какие исключения могут использовать те ресурсы нижнего уровня. Теперь держись!

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

SUMMA SUMMARUM

Таким образом, согласно моей интерпретации catch late означает перехватывать исключения в наиболее удобном месте ГДЕ ВЫ НЕ РАЗРЕШАЕТЕ АБСТРАКЦИЮ ЛЮБОЙ . Не поймите слишком рано! Исследуйте исключения на уровне, где вы создаете конкретное исключение, бросая экземпляры абстракций ресурсов, слой, который знает конкреции абстракций. «Проводка».

НТН. Счастливое кодирование!

ответил Daniel Dinnyes 6 MarpmThu, 06 Mar 2014 19:50:33 +04002014-03-06T19:50:33+04:0007 2014, 19:50:33
10

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

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

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

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

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

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

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

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

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

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

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

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

ответил Stephen 4 MaramTue, 04 Mar 2014 03:59:57 +04002014-03-04T03:59:57+04:0003 2014, 03:59:57
6

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

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

ответил ratchet freak 3 MarpmMon, 03 Mar 2014 16:15:32 +04002014-03-03T16:15:32+04:0004 2014, 16:15:32
4

Допустимым бизнес-правилом является «если программное обеспечение нижнего уровня не может вычислить значение, тогда ...»

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

ответил soru 3 MarpmMon, 03 Mar 2014 17:16:46 +04002014-03-03T17:16:46+04:0005 2014, 17:16:46
2

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

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

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

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

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

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

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

ответил Aschratt 4 MarpmTue, 04 Mar 2014 15:21:19 +04002014-03-04T15:21:19+04:0003 2014, 15:21:19
0

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

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

ответил davidk01 4 MaramTue, 04 Mar 2014 01:26:24 +04002014-03-04T01:26:24+04:0001 2014, 01:26:24

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132