【问题标题】:When is it OK to use exception handling for business logic?什么时候可以对业务逻辑使用异常处理?
【发布时间】:2011-07-19 16:17:13
【问题描述】:

我认为作为 Java(可能还有任何具有异常处理的语言)的一般规则,人们应该尽量避免使用异常处理来实际处理业务逻辑,这是公认的。一般来说,如果预期某种情况应该发生,应该检查它并更直接地处理它,而不是依靠异常处理来为你做检查。例如,以下不被认为是好的做法:

try{
  _map.put(myKey, myValue);
} catch(NullPointerException e){
  _map = new HashMap<String, String>();
}

应该像这样完成延迟初始化:

if(_map == null){
  _map = new HashMap<String, String>();
}
_map.put(myKey, myValue);

当然,可能存在比简单地处理延迟初始化更复杂的逻辑。因此,鉴于这种类型的事情通常不受欢迎......如果有的话,依赖发生的异常来发生某些业务逻辑是一个好主意吗?是否可以准确地说,任何一个人觉得不得不使用这种方法的实例确实突出了所使用的 API 的弱点?

【问题讨论】:

  • 这个“一般”规则有例外。在异常实现非常差的语言中(例如在 Java 中),除了简单的错误处理之外,将它们用于任何其他事情确实不是一个好主意。但是在像 OCaml 这样的语言中,将异常用于控制流是完全可以的,因为它们非常便宜且高效。
  • 这让我想到了 Python 的迭代器。他们依靠StopException 来停止迭代。对this answer 的评论暗示了 Python 哲学“请求宽恕比请求许可更容易”。
  • 所以听起来哲学比我意识到的更多地依赖于语言......有趣......

标签: java exception-handling coding-style


【解决方案1】:

抛出异常在 C++ 中是一项相对昂贵的操作,而在 Java 中则是极其昂贵的操作。仅从效率的角度来看,避免显式检查并接受异常是没有意义的。我想你可能能够在一些罕见的情况下证明它是合理的,即检查是否会抛出异常非常复杂或几乎不可能,但除此之外,我会说答案是“几乎永远不会”。

【讨论】:

  • 更正:异常的实例化很昂贵,而抛出则不然。此外,它并不是“非常”昂贵。
  • 相对于空检查,我会调用 fillInStackTrace() 非常昂贵。投掷也不是那么便宜;虽然它不像在 C++ 中那样涉及调用析构函数,但它确实涉及通过内部数据结构来查找堆栈的每一层的适当处理程序。
【解决方案2】:

只要可以预料到异常但无法避免。

说,如果您依赖某种外部 API 来解析数据,并且该 API 提供了解析方法,但没有说明是否可以解析给定的输入(或者解析是否成功取决于无法控制的因素,但 API 未提供适当的函数调用),并且在无法解析输入时,解析方法会抛出异常。

使用适当设计的 API,这应该归结为“几乎从不”到“从不”范围内的某个数量。

我认为绝对没有理由在代码中使用异常处理作为正常流控制的手段。它很贵,很难阅读(看看你自己的第一个例子;我意识到它可能写得很快,但是当_map 没有被初始化时,你最终得到的是一个空地图,丢弃了条目你试图添加),并且它在代码中散布着大量无用的 try-catch 块,这可以很好地隐藏真正的问题。再次以你自己的例子为例,如果对_map.add() 的调用由于某种原因other 抛出NullPointerException 而不是_mapnull 怎么办?突然间,您正在默默地重新创建一个空地图,而不是向其中添加一个条目。由于意外状态,我敢肯定我真的不必说会导致代码中完全不相关的地方出现任何数量的错误......

编辑:为了清楚起见,上面的答案是在 Java 的上下文中编写的。其他语言可能(并且显然确实)在异常的实现费用方面有所不同,但其他点仍然应该成立。

【讨论】:

  • +1 用于指出为什么在我使用的实际示例中使用错误的做法不好(虽然没有太多,但匆忙放在一起)。
  • 是的,我认为这是仓促的,但它仍然是一个重要的考虑因素。您的示例仅强调了这一点。
  • Python 是在这个问题上反对的一个有趣的例子;请参阅hereWikipediapython.net 上的讨论。
  • 除了,(1)我的答案是在 Java 的上下文中编写的,原始问题的标签表明这是 OP 的主要关注点; (2) 没有在 Python 中工作过,所提出的大部分观点应该仍然成立。例如,错误的高风险,例如 OP 的示例所举例说明的错误。
【解决方案3】:

如果您正在处理实际上是业务逻辑一部分的错误条件,则可以使用异常。例如:

try{
   // register new user
   if(getUser(name) != null)
      throw new MyAppException("Such user already exists");
   //other registration steps......
}catch(MyAppException ex){
   sendMessage(ex.getMessage());
}

【讨论】:

  • 为什么不直接从if 块中调用sendMessage(),完全避免异常呢?如果需要,请输入breakcontinuereturn 或其他适当的流控制语句。这也让事情变得更加清晰。
  • 但是您可以争辩说,调用此函数的人应该自己执行测试。使用检查和异常来保护你的不变量是好的,但这并不意味着你的图书馆的客户不应该这样做来覆盖例外情况。
  • @Michael:我同意,但更现实的例子是异常被抛出给需要知道它发生的调用者。
  • 很抱歉,这不太行。在同一个方法中抛出和捕获异常是非常糟糕的风格并且启动效率非常低。
  • @Mark:在我之前的回答中,这属于“可能预期但无法避免”的类别。您可以预期在用户填写注册表时已创建了另一个同名的用户帐户,但是/并且您无法避免这种可能性,直到您自己实际创建了用户帐户。因此,抛出异常是合适的,尽管并不否定更优雅地检查不变量的需要。但是,我也同意@Ernest,在同一方法中投掷和接球根本不是好习惯。
【解决方案4】:

例外是对象是有原因的。 Java 语言的设计者将所有Throwables 分为两种主要类型也是有原因的:选中和未选中。

依赖 一定会发生异常 业务逻辑发生?

是的。绝对地。您应该阅读“Effective Java, Second Edition”的第 9 章。这一切都在那里。很好的解释,等着你。

【讨论】:

  • 单纯的检查异常与我所描述的不同。我觉得检查异常是针对意外情况,但存在一种合法的方法可以从中恢复。我所描述的更多是当您真正预期“意外”发生时。
  • @Michael McGowan 有时例外是使某些东西有用的唯一方法。想想多线程和阻塞调用。
  • 本网站的目的是提供答案,而不是将某人重定向到另一个来源(与该来源一样好)。这比说 RTFM 好不了多少。 -1.
  • @Mark Peters 以更好的方式做已经完成的事情有什么意义?
  • 首先,不是每个人都有那本书。其次,目标是使该站点成为信息的存储库。你没有回答这个问题,你只是告诉他们到别处寻找答案。这不是一个答案,但它会是一个有效的评论。
【解决方案5】:

这确实是一个讨论问题,答案取决于您的系统设计。

我的直觉永远不会,但我已经看到几个系统使用异常来实现业务错误。我个人觉得这很恶心,但我真的理解一旦你知道它失败就中断业务流程、处理你的失败(例如回滚你的工作单元)并将错误返回给调用者的好处,也许加上信息。

一个可能的缺点是,跨多个不同类进行错误处理非常简单,因此很难从代码中推断出进程失败时发生的情况。

无论如何这里没有单一的答案,您需要权衡这两种方法,有时将它们结合起来。

关于您的示例,我认为使用异常进行流控制没有任何优势,尤其是在“良好”(设计为工作)场景中。

【讨论】:

    猜你喜欢
    • 2016-11-14
    • 2012-07-18
    • 1970-01-01
    • 2012-05-20
    • 1970-01-01
    • 2011-02-07
    • 2011-03-17
    • 2011-09-07
    • 1970-01-01
    相关资源
    最近更新 更多