it-swarm.com.ru

Является ли возврат исключения анти-паттерном?

У меня есть два простых метода:

public void proceedWhenError() {
   Throwable exception = serviceUp();

   if (exception == null) {
      // do stuff
   } else {
      logger.debug("Exception happened, but it's alright.", exception)
      // do stuff
   }
}

public void doNotProceedWhenError() {
   Throwable exception = serviceUp();

   if (exception == null) {
      // do stuff
   } else {
      // do stuff
      throw new IllegalStateException("Oh, we cannot proceed. The service is not up.", exception)
   }
}

Третий метод - это приватный вспомогательный метод:

private Throwable serviceUp() {
    try {
        service.connect();
        return null;
    catch(Exception e) {
       return e;
    }
}

Мы поговорили с моим коллегой о шаблоне, использованном здесь:

возвращая Exception (или Throwable) объект из метода serviceUp().

Первое мнение:

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

Второе мнение:

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

Как вы думаете, 1) или 2) правильно и особенно, почему? Обратите внимание, что вопрос касается ТОЛЬКО метода serviceUp() и его типа возврата - boolean vs Exception object.

Примечание. Я не задаюсь вопросом, использовать ли объекты Throwable или Exception.

19
Martin Linha

Запрещается использовать исключения для направления потока, только когда исключение выдается в неисключительной ситуации.*, Например, завершение цикла с помощью исключения, когда вы достигаете конца коллекции, является анти-паттерном.

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

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

public class CallStatus {
    private final Exception serviceException;
    private final boolean isSuccess;
    public static final CallStatus SUCCESS = new CallStatus(null, true);
    private CallStatus(Exception e, boolean s) {
        serviceException = e;
        isSuccess = s;
    }
    public boolean isSuccess() { return isSuccess; }
    public Exception getServiceException() { return serviceException; }
    public static CallStatus error(Exception e) {
        return new CallStatus(e, false);
    }
}

Теперь вызывающая сторона получит CallStatus от serviceUp:

CallStatus status = serviceUp();
if (status.isSuccess()) {
    ... // Do something
} else {
    ... // Do something else
    Exception ex = status.getException();
}

Обратите внимание, что конструктор является закрытым, поэтому serviceUp либо вернет CallStatus.SUCCESS, либо вызовет CallStatus.error(myException).

* Что является исключительным, а что не исключительным, во многом зависит от контекста. Например, нечисловые данные вызывают исключение в Scanner's nextInt, поскольку считают такие данные недействительными. Однако те же точные данные не вызывают исключение в методе hasNextInt, потому что они совершенно допустимы.

30
dasblinkenlight

Второе мнение ("все в порядке") не имеет места. Код не в порядке, потому что возвращать исключения, а не бросать их, не совсем идиоматично.

Я также не покупаю первое мнение ("использование исключений для управления рабочим процессом - это анти-паттерн"). service.connect() вызывает исключение, и вы должны реагировать на это исключение - так что это is эффективно управляет потоком. Возвращать boolean или какой-либо другой объект состояния и обрабатывать его вместо обработки исключения - и думать, что это не поток управления, основанный на исключении, наивно. Еще один недостаток заключается в том, что если вы решите выбросить исключение (в IllegalArgumentException или что-то еще), у вас больше не будет исходного исключения. И это очень плохо, когда вы пытаетесь проанализировать, что на самом деле произошло.

Так что я бы сделал классику:

  • Бросьте исключение в serviceUp.
  • В методах, вызывающих serviceUp:
    • try-catch, отладка журнала и проглотить исключение, если вы хотите продолжить на исключение.
    • try-catch и сбросьте исключение, заключенное в другое исключение, предоставляя дополнительную информацию о том, что произошло. Или просто позвольте исходному исключению распространяться через throws, если вы не можете добавить ничего существенного.

Наиболее важно не потерять оригинальное исключение.

17
lexicore

Оба не правы

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

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

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

И новые программисты будут бесконечно смущены и/или скопируют этот анти-шаблон в места, где он просто не принадлежит.

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

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

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

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

Так, в случае вашей serviceUp(), либо сделайте его void - он либо работает 99% времени, либо выдает исключение; или сделайте его верным boolean, если вы полностью согласны с тем, что он где-то потерпит неудачу Если вам нужно передать сообщение об ошибке, сделайте это как String, или сохраните его где-нибудь в другом месте, или как-то еще, но не используйте его как возвращаемое значение, особенно если вы не собираетесь throw повторить это позже.

Простое стандартное решение

Это решение короче (меньше строк, меньше переменных, меньше if), проще, более стандартно и выполняет именно то, что вы хотели. Легко поддерживать, легко понять.

public void proceedWhenError() {
   try {
      serviceUp();
      // do stuff (only when no exception)
   }
   catch (Exception exception) {
      logger.debug("Exception happened, but it's alright.", exception)
      // do stuff (only when exception)
   }
}

public void doNotProceedWhenError() {
   try {
      serviceUp();
      // do stuff (only when no exception)
   }
   catch (Exception exception) {
      // do stuff (only when exception)
      throw new IllegalStateException("Oh, we cannot proceed. The service is not up.", exception)
   }
}

private void serviceUp() {
    service.connect();
}
7
AnoE

Я бы возвратил ServiceState, который может быть, например, RUNNING, WAITING, CLOSED. Метод будет называться getServiceState.

enum ServiceState { RUNNING, WAITING, CLOSED; }

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

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

Пример:

public void proceedWhenError() {
   final ServiceState state = getServiceState();

   if (state != ServiceState.RUNNING) {
      logger.debug("The service is not running, but it's alright.");
   }
   // do stuff
}

public void doNotProceedWhenError() {
   final ServiceState state = getServiceState();

   if (state != ServiceState.RUNNING) {
      throw new IllegalStateException("The service is not running...");
   }
   // do stuff
}

private ServiceState getServiceState() {
    try {
        service.connect();
        return ServiceState.RUNNING;
    catch(Exception e) {
        // determine the state by parsing the exception
        // and return it
        return getStateFromException(e);
    }
}

Если исключения, выданные службой, важны и/или обработаны в другом месте, они вместе с состоянием могут быть сохранены в объект ServiceResponse:

class ServiceResponse {

    private final ServiceState state;
    private final Exception exception;

    public ServiceResponse(ServiceState state, Exception exception) {
        this.state = state;
        this.exception = exception;
    }

    public static ServiceResponse of(ServiceState state) {
        return new ServiceResponse(state, null);
    }

    public static ServiceResponse of(Exception exception) {
        return new ServiceResponse(null, exception);
    }

    public ServiceState getState() {
        return state;
    }

    public Exception getException() {
        return exception;
    }

}

Теперь с ServiceResponse эти методы могут выглядеть так:

public void proceedWhenError() {
   final ServiceResponse response = getServiceResponse();

   final ServiceState state = response.getState();
   final Exception exception = response.getException();

   if (state != ServiceState.RUNNING) {
      logger.debug("The service is not running, but it's alright.", exception);
   }
   // do stuff
}

public void doNotProceedWhenError() {
   final ServiceResponse response = getServiceResponse();

   final ServiceState state = response.getState();
   final Exception exception = response.getException();

   if (state != ServiceState.RUNNING) {
      throw new IllegalStateException("The service is not running...", exception);
   }
   // do stuff
}

private ServiceResponse getServiceResponse() {
    try {
        service.connect();
        return ServiceResponse.of(ServiceState.RUNNING);
    catch(Exception e) {
        // or -> return ServiceResponse.of(e);
        return new ServiceResponse(getStateFromException(e), e);
    }
}
3
Andrew Tobilko

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

Мой инстинкт подсказывает такой подход:

// Use an action name instead of a question here because it IS an action.
private void bringServiceUp() throws Exception {

}

// Encapsulate both a success and a failure into the result.
class Result {
    final boolean success;
    final Exception failure;

    private Result(boolean success, Exception failure) {
        this.success = success;
        this.failure = failure;
    }

    Result(boolean success) {
        this(success, null);
    }

    Result(Exception exception) {
        this(false, exception);
    }

    public boolean wasSuccessful() {
        return success;
    }

    public Exception getFailure() {
        return failure;
    }
}

// No more cognitive dissonance.
private Result tryToBringServiceUp() {
    try {
        bringServiceUp();
    } catch (Exception e) {
        return new Result(e);
    }
    return new Result(true);
}

// Now these two are fine.
public void proceedWhenError() {
    Result result = tryToBringServiceUp();
    if (result.wasSuccessful()) {
        // do stuff
    } else {
        logger.debug("Exception happened, but it's alright.", result.getFailure());
        // do stuff
    }
}

public void doNotProceedWhenError() throws IllegalStateException {
    Result result = tryToBringServiceUp();
    if (result.wasSuccessful()) {
        // do stuff
    } else {
        // do stuff
        throw new IllegalStateException("Oh, we cannot proceed. The service is not up.", result.getFailure());
    }
}
1
OldCurmudgeon

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

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

1
supercat

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

представьте, есть ли ошибка в коде serviceUp(), которая заставляет его генерировать NullPointerException. Теперь представьте, что ошибка находится в службе, и то же самое NullPointerException выбрасывается из connect().

Видишь мою точку зрения?

Другая причина - изменение требований.

В настоящее время сервис имеет два условия: либо вверх, либо вниз.
В настоящее время.

Завтра у вас будет три условия для обслуживания: вверх, вниз. или работает с предупреждениями. На следующий день вы также захотите, чтобы метод возвращал подробности об услуге в json .....

1
Sharon Ben Asher

Как упоминалось в предыдущих комментариях "Исключение - это событие", объект исключения, который мы получаем, является лишь деталью события. Как только исключение перехватывается в блоке catch и не перебрасывается, событие заканчивается. Сообщите, что у вас просто есть подробный объект, а не исключение, хотя объект имеет класс исключение/throwable.

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

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

0
dev. K

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

1. Вернуть логическое значение

Первый - вернуть boolean. Этот стиль широко используется в API C и C, таких как PHP. Это приводит - например, в PHP - часто к такому коду:

if (xyz_parse($data) === FALSE)
    $error = xyz_last_error();

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

2. Используйте объект-посредник/состояние/результат

Если вы не используете исключения, у вас все еще есть возможность в OOP языках возвращать объекты , которые описывают состояние .

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

ValidationResult result = parser.validate(data);
if (result.isValid())
    // business logic
else
    error = result.getvalidationError();

3. Используйте исключения

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

Применяя это

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

Использовать (проверено) исключения

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

public void connectToService() thows IOException {
    // yada yada some init stuff, whatever
    socket.connect();
}

public void proceedWhenError() {
   try {
        connectToService();
   } else {
      logger.debug("Exception happened, but it's alright.", exception)
      // do stuff
   }
}

public void doNotProceedWhenError() throws IllegalStateException {
    try  {
        connectToService();
        // do stuff
    }
    catch(IOException e) {
      throw new IllegalStateException("Oh, we cannot proceed. The service is not up.", exception)
   }
}

Используйте boolean

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

public boolean isServiceUp {
    return ...; // query the service however you need
}

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

Использовать объекты посредник/результат/состояние

Итак, давайте отклоним этот ограниченный подход и переделаем его, используя объект-посредник/результат/состояние:

class ServiceState {
    enum State { CONNECTED, WAITING, CLOSED; }

    State state;
    IOException exception;
    String additionalMessage;

    public ServiceState (State state, IOException ex, String msg) {
         [...] // ommitted for brevity
    }

   // getters ommitted for brevity
}

Вспомогательная функция станет getServiceState:

public ServiceState getServiceState() {
    // query the service state however you need & return
}

public void proceedWhenError() {
   ServiceState state = getServiceState();

   if (state.getState() == State.CONNECTED) {
      // do stuff
   } else {
      logger.debug("Exception happened, but it's alright.", state.getException())
      // do stuff
   }
 }

public void doNotProceedWhenError() {
    ServiceState state = getServiceState();

   if (state.getState() == State.CONNECTED) {
      // do stuff
   } else {
      // do stuff
      throw new IllegalStateException("Oh, we cannot proceed. The service is not up.", exception)
   }
}

Обратите внимание, что вы также повторяете себя в proceedWhenError, весь код может быть упрощен до:

public void proceedWhenError() {
   ServiceState state = getServiceState();

  if (state.getState() != State.CONNECTED) {
      logger.debug("Exception happened, but it's alright.", state.getException())

   }
 // do stuff
}

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

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

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

Однако просто возвращать boolean не рекомендуется, поскольку оно слишком ограничено. Если все вам нужно знать , работает ли служба или нет (и не заинтересованы в причине), такая Метод может все же быть полезным, хотя (и мог бы, за кулисами, быть реализован как вспомогательный метод, который просто выполняет return getServiceState() == State.SUCCESS).


Это анти-шаблон, чтобы использовать исключения для управления рабочим процессом

Итак, давайте посмотрим, что является исключением?

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

Источник: https://docs.Oracle.com/javase/tutorial/essential/exceptions/definition.html

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

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

0
Polygnome

Это анти-паттерн:

catch(Exception e) {
   return e;
}

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

Но в вашем сценарии эта стоимость уже оплачена. Поймать исключение от имени вашего абонента никто не одобряет.


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

0
Ben Voigt

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

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

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

0
vincrichaud