返回异常反模式?

时间:2018-04-12 12:26:53

标签: java exception error-handling exception-handling anti-patterns

我有两个简单的方法:

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())对象。

第一个意见:

  

使用Exceptions控制工作流是一种反模式,我们只应该从中返回boolean   serviceUp()而不是Exception对象本身。争论是   使用异常来控制工作流是一种反模式。

第二种意见:

  

没关系,因为我们需要在之后处理对象   两个第一个方法不同,是否返回Exception对象   或布尔值根本不会改变工作流程

你认为1)或2)是否正确?尤其是,为什么? 请注意,问题仅限于方法serviceUp()及其返回类型 - boolean vs Exception对象。

注意:我不会质疑是否使用Throwable或Exception对象。

11 个答案:

答案 0 :(得分:30)

只有在异常情况 * 中抛出异常时才使用异常来引导流,这是一种反模式。例如,当到达集合的末尾时通过抛出异常来结束循环是一种反模式。

另一方面,使用实际异常控制流是一种很好的异常应用。如果您的方法遇到无法处理的异常情况,则应抛出异常,从而将调用者中的流重定向到异常处理程序块。

返回"裸体"来自方法的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 nextInt中出现异常,因为它认为此类数据无效。但是,相同的确切数据不会导致hasNextInt方法中的异常,因为它完全有效。

答案 1 :(得分:17)

第二种意见("它没关系")不成立。代码不正常,因为返回异常而不是抛出异常并不是真的。

我也不会购买第一个意见("使用Exceptions控制工作流程是反模式")。 service.connect()抛出一个异常,你必须对这个异常做出反应 - 所以这个 有效地控制了流量。返回boolean或其他一些状态对象并处理它而不是处理异常 - 并且认为它不是基于异常的控制流是天真的。另一个缺点是,如果您决定重新抛出异常(包含在IllegalArgumentException或其他内容中),您将不再拥有原始异常。当你试图分析实际发生​​的事情时,这是非常糟糕的。

所以我做了经典之作:

  • serviceUp
  • 中抛出异常
  • 在调用serviceUp的方法中:
    • try-catch,如果要继续执行异常,请记录调试并吞下异常。
    • try-catch并重新抛出包含在另一个异常中的异常,提供有关所发生事件的更多信息。或者,如果您无法添加任何实质内容,只需通过throws传播原始异常。

最重要的是不要失去原来的例外。

答案 2 :(得分:7)

两者都错了

  

没关系,因为我们需要在两个第一个方法中以不同的方式处理对象,并且返回Exception对象或布尔值是否根本不会改变工作流程

不行。例外的概念意味着它们被抛入,例外情况。它们应该被捕获到它们将被处理的地方(或者至少在一些本地清理/记录/等之后重新抛出)。它们并不意味着像这样(也就是说,在"域和#34;代码中)。

人们会感到困惑。真正的错误可能很容易蔓延 - 例如,如果来自其他来源的Exception来自网络,那该怎么办?一个你没想到的,那真的很糟糕(就像你因某些编程错误而产生的一些越界异常)?

新鲜的程序员将无休止地混淆,和/或将反模式复制到它不属于的地方。

例如,一位同事最近实现了一个非常复杂的界面(如机器到机器界面,而不是Java interface),他做了类似的异常处理;将异常转换为静默忽略的变体(以某些日志消息为模)。毋庸置疑,他实际上没有想到的任何例外以最糟糕的方式打破了整个混乱局面;与fail fast相反。

  

使用Exceptions控制工作流是一种反模式,我们只应该从serviceUp()返回boolean,而不是从Exception对象本身返回。该论点是使用异常来控制工作流是一种反模式。

例外情况肯定会控制工作流程,因为它们经常会中止它,或者重定向到显示给用户的错误消息。完全可以使用部分代码"禁用"由于例外;也就是说,除了处于最高级别的控制之外,肯定允许在其他地方进行异常处理。

但返回异常确实是一种反模式;没有人期望,这很奇怪,它会导致虚假错误(很容易忽略返回值)等等。

所以,对于你的serviceUp(),要么使它成为void - 它要么在99%的时间内工作,要么抛出异常;或者说它是真的boolean,因为你完全接受它会在某个地方失败。如果您确实需要传递错误消息,请将其作为String执行,或者将其保存在某个地方或其他地方,但不要将其用作返回值,尤其是如果您打算{{} 1}}以后再说。

简单,标准的解决方案

此解决方案更短(线数更少,变量更少,throw更少),更简单,沼泽标准,并且完全符合您的要求。易于维护,易于理解。

if

答案 3 :(得分:3)

我会返回ServiceState,例如RUNNINGWAITINGCLOSED。该方法将命名为getServiceState

enum ServiceState { RUNNING, WAITING, CLOSED; }

我从未见过因执行而返回异常的方法。对我来说,当一个方法返回值时,这意味着该方法完成了它的工作没有问题。我不想检索结果并解析它包含任何错误。结果本身意味着没有发生故障 - 一切都按计划进行。

另一方面,当方法抛出异常时,我需要解析特殊对象以找出出错的地方。我不解析结果,因为没有结果

一个例子:

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

答案 4 :(得分:1)

返回Exception确实是一种反模式,因为异常应该保留给执行中的错误,而不是描述服务的条件。

想象一下serviceUp()代码中是否存在导致其抛出NullPointerException的错误。现在假设错误在服务中,并且从NullPointerException抛出相同的connect()

看到我的观点?

另一个原因是需求变化。

目前,该服务有两个条件:向上或向下 的目前

Tommorow,您将有三个服务条件:向上,向下。或有警告功能。第二天,您还希望该方法在json中返回有关服务的详细信息.....

答案 5 :(得分:1)

明显的认知失调是这里的反模式。读者会看到您使用流控制的异常,开发人员会立即尝试重新编码,因此不会。

我的直觉暗示了一种方法:

// 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());
    }
}

答案 6 :(得分:1)

如果方法的调用者期望操作可能成功或失败,并且准备处理任何一种情况,那么该方法应该通常通过返回值指示操作是否失败。如果调用者没有准备好有效地处理失败,那么发生失败的方法应抛出异常而不是要求调用者添加代码来执行此操作。

一个问题是,有些方法会让一些准备优雅地处理故障的呼叫者和其他不能处理故障的呼叫者。我首选的方法是让这些方法接受一个可选的回调函数,该函数在失败的情况下被调用;如果没有提供回调,则默认行为应该是抛出异常。在调用者准备处理故障的情况下,这种方法可以节省构造异常的成本,同时最大限度地减少呼叫者的负担。这种设计最大的困难在于决定回调应该采用哪些参数,因为以后更改这些参数往往很困难。

答案 7 :(得分:0)

您无法告诉2)意见是错误的,因为工作流程将按您希望的方式运行。从逻辑的角度来看,它将按照需要运行,因此它是正确的。

但这是一个非常奇怪且不推荐的方法。首先是因为Exception不是为了这样做而设计的(这意味着你正在做一个反模式)。它是一个旨在抛出和捕获的特定对象。所以它很奇怪,而不是按照设计使用它,选择它返回它而宁可抓住它使用if on it。此外,您(可能它可以忽略不计)面对性能问题而不是使用简单的布尔值作为标志,您实例化整个对象。

最后,如果函数的目标是获取某些东西(这不是你的情况),也不推荐使用cause函数。你应该将它重新设计为一个启动服务的函数,然后它不返回任何因为它不会被调用来获取某些东西,并且如果发生错误它将抛出异常。如果您想知道您的服务工作是否创建了一个旨在为您提供public boolean isServiceStarted()等信息的功能。

答案 8 :(得分:0)

有三个(惯用)处理在运行时可能失败的函数。

1。返回布尔值

第一个是返回boolean。这种风格广泛用于C和C风格的API,如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)可能确实查询< / em>服务是否正在运行(可能是由其他人启动的)。在这种情况下,使用布尔值将是正确的方法

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是不可取的,因为它太有限了。如果 all 你需要知道 服务是否正在运行(并且对原因不感兴趣),这样的方法可能仍然虽然有用,但是可以在幕后实现为return getServiceState() == State.SUCCESS的辅助方法。

  

使用例外来控制工作流程是一种反模式

那么,让我们看看 是一个例外吗?

  

定义:异常是在程序执行期间发生的事件,会破坏程序指令的正常流程

来源:https://docs.oracle.com/javase/tutorial/essential/exceptions/definition.html

异常的定义是它是 disprupts (从而改变)程序工作流的事件。如果使用定义的是反模式,则整个语言特征本身必须被视为反模式。有些人认为异常是坏的,某些一些有效的参数,但一般来说,异常被认为是有用的构造并被视为处理特殊工作流程的有用工具。

投掷异常肯定不是反模式。另一方面,返还是。因此,您的代码 - 如所呈现的 - 是非惯用的。

答案 9 :(得分:0)

这是一种反模式:

Throwable

返回try的唯一合理理由是向预期出现故障的快速路径提供详细的故障信息,而无需支付异常处理的价格 1 。作为奖励,如果调用者不知道如何处理这种特殊情况,它可以很容易地转换为抛出的异常。

但在您的方案中,已经支付了该费用。代表您的来电者捕获例外不会有任何好处。

1 小心翼翼地,捕获异常的直接成本(找到匹配的处理程序,堆栈展开)非常低或者无论如何都需要完成。与返回值相比,catch / +的大部分成本都是偶然的操作,例如构建堆栈跟踪。因此,未被丢失的异常对象是否能为有效的错误数据提供良好的存储空间取决于这种偶然的工作是在构造时还是在投掷时完成的。因此,在不同的托管平台之间返回异常对象是否合理可能会有所不同。

答案 10 :(得分:0)

正如前面评论中提到的“异常是一个事件”我们得到的异常对象只是事件的细节。 一旦异常被捕获到“catch”块并且没有被重新抛出,事件就结束了。 发布你只是有一个详细对象而不是例外,尽管该对象属于class exception / throwable。

返回异常对象可能是有用的,因为它保留了详细信息,但它会增加歧义/混淆,因为调用方法不是“处理异常并根据异常控制流”。它只是使用返回对象的细节来做出决定。

所以在我看来,一种更合乎逻辑且更少混淆的方法是根据异常返回一个布尔/枚举,而不是简单地返回异常对象。