V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
OneAPM
V2EX  ›  程序员

除非万不得已,别 Catch!

  •  7
     
  •   OneAPM ·
    oneapm · 2015-07-16 18:41:14 +08:00 · 6055 次点击
    这是一个创建于 3205 天前的主题,其中的信息可能已经有所发展或是发生改变。

    [编者按] 作者 Yegor Bugayenko 是 Teamed.io 的软件架构师,热衷于软件质量研究和有效的项目管理方法探索。在本文中,Yegor 就「异常被捕获但并未重新抛出」这个问题进行了深入讨论,并分享了一些建议。

    对异常只捕获但并未重新抛出究竟是 anti-pattern,还是个普通而且非常流行的错误确实无从考究。但毫无疑问的是,在所有异常捕获代码中,它基本无处不在,正如下面这段代码:

    try {
      stream.write(data);
    } catch (IOException ex) {
      ex.printStackTrace();
    }
    

    ©Catch Me If You Can (2002) by Steven Spielberg
    注意:下面的代码并没有反对。

    try {
      stream.write('X');
    } catch (IOException ex) {
      throw new IllegalStateException(ex);
    }
    

    这叫做 exception chaining,是一个非常有效的构造。

    那么,捕获异常并记录究竟存在什么样的问题?首先,从宏观着手,这里正在谈论的是面向对象编程——意味着需要处理的是对象。一个对象(准确的说,是它的类)应该是这个样子:

    final class Wire {
      private final OutputStream stream;
      Wire(final OutputStream stm) {
       this.stream = stm;
     }
     public void send(final int data) {
        try {
          this.stream.write(x);
        } catch (IOException ex) {
          ex.printStackTrace();
        }
      }
    }
    

    这里这样来应用这个类的:

    new Wire(stream).send(1);
    

    看起来不错,对么?当调用 send(1) 时,并不需要担心出现 IOException ,它将在方法内部处理。同时,如果出现异常,异常信息会被记录。但是这么做的理念是完全错误的,它传承自没有异常处理的语言,比如 C。

    异常的发明是为了将整个错误处理代码从主要逻辑中移除,以此来简化设计。同时,它们不仅仅是被移走,而且被集中在一个地方——在 main() 方法中,即整个应用的入口。

    一个异常的主要目的是搜集尽可能多的错误信息并将它抛到最上层,在这里用户能够针对它做一些处理。Exception chaining 则可以帮助更多,它允许在异常抛至上层的过程中扩充错误信息。在这个过程中,实际上非常类似于每次捕获到泡泡(即异常)后,所做的只是将它添加到一个更大的泡泡中然后重新抛出。当它到达最高层的时候,那里就有许多泡泡,像一个 Russian Doll,一个嵌套着另外一个。最原始的异常就是最小的那个泡泡。

    当捕获一个异常而并没有重新抛出时,等同于你捏碎了这个泡泡。其中包含的大量信息,包括最原始的异常和所有其它带有信息的泡泡,都被你牢牢的抓在手中。你杜绝为上层呈现它,同时你如何处理和使用上层也毫无察觉。这一切都像是暗箱操作,一些潜在的重要信息被隐藏。

    因此,在这里直接导致的是 send() 方法无法得到信任,同样基于 send() 方法的操作也无法得到信任,对象之间的信任链被破坏殆尽。这里的建议是尽可能少捕获异常,同时一旦捕获则必须抛出。

    不幸的是,Java 在很多地方的设计违背了这个原则。例如,Java 有需检查和不需检查的异常两类,但是在我看来,只应该有需检查的异常(这些异常必须被捕获或者声明为 throwable)。而且,Java 允许在一个方法中将多个异常类型声明为 throwable ——这是另一个错误;坚持只声明一种类型。在层次结构的顶部有一个通用的 Exception 类,在我看来也是错误的。除此之外,一些内置的类不允许抛出任何需检查的异常,比如说Runnable.run()Java 还有许多关于异常的问题。

    但是尝试记住这些原则,你的代码将会更加整洁:catch 除非你别无选择。

    P.S.这个类应该是这个样子:

    final class Wire {
      private final OutputStream stream;
      Wire(final OutputStream stm) {
        this.stream = stm;
      }
      public void send(final int data)
        throws IOException {
        this.stream.write(x);
      }
    }
    

    原文链接:Catch Me If You ... Can't Do Otherwise

    本文系 OneAPM 工程师编译整理。OneAPM 是应用性能管理领域的新兴领军企业,能帮助企业用户和开发者轻松实现:缓慢的程序代码和 SQL 语句的实时抓取。想阅读更多技术文章,请访问 OneAPM 官方博客

    30 条回复    2015-07-18 22:14:05 +08:00
    hitsmaxft
        1
    hitsmaxft  
       2015-07-16 21:54:46 +08:00
    基础库一般是尽可能地抛出异常,不然日志会很乱, 而业务模块尽可能地 catch 异常和合理地记录异常日志,保证不影响其他模块

    因为一个小问题把页面500了,老板也只能 throw 掉你了。
    zonghua
        2
    zonghua  
       2015-07-16 22:08:33 +08:00
    @hitsmaxft 菜鸟好累,做的东西都是虫,代码质量十分不堪。
    xcv58
        3
    xcv58  
       2015-07-16 22:21:36 +08:00   ❤️ 1
    作者吐槽了一通,并没给出解决办法。
    invite
        4
    invite  
       2015-07-16 22:25:44 +08:00
    呵呵, send() 不能信任这个结论, 就这么突然间的出来了? 这文章也不能信任.
    dndx
        5
    dndx  
       2015-07-17 03:36:50 +08:00 via iPhone
    就让它崩溃原则。
    Yiph
        6
    Yiph  
       2015-07-17 09:26:35 +08:00
    神一样的翻译系列,建议大家看后面附的原文链接。
    vivisidea
        7
    vivisidea  
       2015-07-17 09:31:38 +08:00
    @hitsmaxft 赞同这个说法,Controller 还往页面上 throw exception 是作死
    mouhong
        8
    mouhong  
       2015-07-17 10:22:53 +08:00
    作者其实也提到了在最上层 (main) catch 住。

    不过什么是真的“异常”也要分清楚,有些是“可预期的错误”,这种并不是异常,无法预期的才是异常。当一个不可预期错误导致应用进入不可知的状态,自然要提示错误咯 (Web中友好地进行500错误提示)。正确的 throw 和 catch 异常,是不会导致一个小问题把页面 500 的。Controller 通常是最上层,再往页面抛异常是不太对的,但是在有 AOP 支持的框架里,还是可以往上抛的 (上面还有一层可以统一处理)。
    zhicheng
        9
    zhicheng  
       2015-07-17 10:27:58 +08:00 via Android
    除非万不得已,别 Code !
    Agromania
        10
    Agromania  
       2015-07-17 10:47:15 +08:00
    除非万不得已,别当程序员!
    sobigfish
        11
    sobigfish  
       2015-07-17 10:57:42 +08:00
    除非万不得已,别看“翻译”的!
    bengol
        12
    bengol  
       2015-07-17 11:01:31 +08:00 via Android
    这个公司的工程师翻译不行啊
    bobai
        13
    bobai  
       2015-07-17 11:07:16 +08:00
    @sobigfish 看了原文,感觉除了翻译的有些怪异外,没什么不同啊?基本意思没看出来有变化
    sobigfish
        14
    sobigfish  
       2015-07-17 11:12:46 +08:00
    @bobai "注意:下面的代码并没有反对。" 是真的很怪啊。。。
    asj
        15
    asj  
       2015-07-17 11:35:35 +08:00   ❤️ 1
    除非万不得已 {
    不要throw exception
    除非万不得已 {
    不要catch exception
    }
    }

    结论是只有亿不得已的情况才catch
    Air_Mu
        16
    Air_Mu  
       2015-07-17 11:37:22 +08:00
    only if you can
    buru
        17
    buru  
       2015-07-17 12:37:46 +08:00
    我觉得作者说的很对
    Controller的基类应该有一个入口统一进行catch
    具体的Controller就可以往上抛异常了
    supernova16
        18
    supernova16  
       2015-07-17 12:53:50 +08:00
    说翻译的,我觉得技术分享能做到不出错误就值得支持并鼓励了,这样才有更多的好文章得到翻译和传播,能这样做的公司不多
    williamx
        19
    williamx  
       2015-07-17 13:59:04 +08:00
    一种基于异常的语言,很难想象其本质正常。
    incompatible
        20
    incompatible  
       2015-07-17 14:01:56 +08:00
    @williamx 还有什么比异常更好的方式?
    tt7
        21
    tt7  
       2015-07-17 14:13:30 +08:00 via iPad
    标题党, 不是不 catch 而是不要 silent catch.
    alangz
        22
    alangz  
       2015-07-17 17:08:07 +08:00
    什么才是万不得已的时候
    huijiewei
        23
    huijiewei  
       2015-07-17 18:16:46 +08:00
    @invite 因为 send 方法内部把异常消化掉了。造成了就算出现了异常,外面调用的代码也以为这个调用是成功的,所以说 send 方法是不可信的。
    nevermlnd
        24
    nevermlnd  
       2015-07-17 18:19:51 +08:00
    只看出图片是猫鼠游戏
    tinkerer
        25
    tinkerer  
       2015-07-17 19:22:16 +08:00 via iPhone
    注意:下面的代码并没有反对
    invite
        26
    invite  
       2015-07-17 20:25:19 +08:00
    @huijiewei 成功与否,不应该看返回值么?为什么不是返回void导致的,而一定要归结于异常捕获?
    msg7086
        27
    msg7086  
       2015-07-17 20:29:49 +08:00
    @invite
    @williamx
    不能滥用异常,但是也不应该完全没异常啊。
    stevegy
        28
    stevegy  
       2015-07-17 21:19:00 +08:00
    我们规定:界面上*不允许*出现Stack Print。
    - - 这样的规定会直接迫使所有*最外层*的函数使用try/catch/finally,然后catch内部*必须*使用*统一*提供的logger来记录异常的stack message。统一提供的logger不需要每个程序员关心,一般有资深的工程师提供,根据应用的情况,logger有可能是文件,也可能是专用的logger server,但接口一致。
    Comdex
        29
    Comdex  
       2015-07-18 18:21:23 +08:00
    异常和错误如何区分?
    huijiewei
        30
    huijiewei  
       2015-07-18 22:14:05 +08:00 via iPhone
    @invite 他举例的代码可以看到是基础层的代码,和业务无任何关联,抛出异常明显比用返回值更好,因为错误信息更加全面
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   2779 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 15:42 · PVG 23:42 · LAX 08:42 · JFK 11:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.