除非萬不得已,別 Catch!
原文鏈接:Catch Me If You ... Can't Do Otherwise
作者 Yegor Bugayenko 是 Teamed.io 的軟件架構師,熱衷于軟件質量研究和有效的項目管理方法探索。在本文中,Yegor 就 「異常被捕獲但并未重新拋出」 這個問題進行了深入討論,并分享了一些建議。
對異常只捕獲但并未重新拋出究竟是 anti-pattern,還是個普通而且非常流行的錯誤確實無從考究。但毫無疑問的是,在所有異常捕獲代碼中,它基本無處不在,正如下面這段代碼:
try {
stream.write(data);
} catch (IOException ex) {
ex.printStackTrace();
}
注意:下面的代碼并沒有反對。
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 官方博客 。