一文帶你了解如何優雅的處理錯誤邏輯
程序的健壯性
程序在運行的時候總是不可避免地遇到各種錯誤。這些錯誤有一些是包含在原有的邏輯判斷中的。而有一些是被程序描述了,但是我們并不認為它是正常邏輯的一部分。不論是什么形式的問題,我們在進行我們預期的業務邏輯編程的同時,不可避免地要為程序的異常進行編程。
為了在異常中保護程序的流程的正確性,以至于不會出現“過分”的功能不可用,我們要為了異常編程。從而保證了程序的健壯性。
一般來說,如果我們能覆蓋系統所有的異常,那么我們的程序就將變得十分的堅不可摧。顯然,這時候程序的健壯性就得到了體現。
不可放棄的可讀性
程序的健壯性是十分的必要的,因為這關系到系統的穩定性。所以,系統的健壯性是我們不能拋棄的。但不得不說的是,如果我們只關心系統的健壯性,那我們很容易就會讓代碼變得十分的糟糕,難以閱讀。
舉個例子,不論是不是出自自己的手里,我們有時候會看見這種代碼:
A a = getA();
if(a.status != xxx ){
B b = a.getB();
if(b.status != xxx ){
doSomething(b);
}
}
從程序的角度上判斷,當程序的返回結果不滿足主要流程的需要的時候,我們就不執行我們的邏輯。從程序的健壯性的角度上來說,上面的這部分代碼已經滿足了業務的需要。但是他有什么問題呢?如果我們將狀態的判斷去除,就可以簡化為以下邏輯:
A a = getA();
B b = a.getB();
doSomething(b);
這個邏輯就非常清晰了,分為三個部分:獲取變量a,然后獲取a的屬性b,然后根據b進行某種操作。盡管上下兩段代碼在正常流程上的處理是一樣的,而上面的代碼更是對異常流程進行了處理。但是我們很容易就能發現其中的問題:
不優雅的處理異常邏輯,會導致程序變得難以閱讀。
盡管我們處理了異常,但是如果代價是讓我們的代碼邏輯混亂難以閱讀。那么我認為這就不是一個的處理方式。
優雅的處理錯誤邏輯
我們希望在保證不損失程序可讀性的同時保證系統的健壯性。而一些編程的準則能幫助我們達到目標。
使用異常
我們有很多重的方法來描述程序是否滿足我們的預期。比如上文中的第一個例子,我們是通過A、B對象中的狀態字段來體現邏輯是否按照我們的預期來實現的。這個例子里面的字段并不是那么合理,但有時這種處理的方式是因為對象本身的屬性包含這種描述狀態的字段,而我們就直接復用了這些字段用于在后續邏輯中對非期望數據進行處理。但是這樣就存在問題:
- 我們將處理異常的邏輯和處理正常邏輯的代碼混合編排。
- 由于狀態分散在不同的對象中,我們需要在對象被使用前為每個對象進行判斷。
第一個問題導致代碼的可讀性變得比較差。第二個問題導致到進行上有編碼的時候,會依賴下層的細節。如果編碼人不知道下層異常邏輯,則會錯過對異常的處理。
而如果我們用異常來處理則代碼就可以變成:
try{
A a = getA();
B b = a.getB();
doSomething(b);
} catch (AStatusException e) {
e.printStackTrace();
} catch (BStatusException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
可以看到這種處理失敗流程的方式可以可以讓“正常業務邏輯”保持完整,其中try中包裹的部分和上文中不做異常判斷的部分是一模一樣的,所以:
推薦使用異常(Exception)代替失敗狀態,可以保證正常業務邏輯的完整性。
進一步的,在《如何寫好一個方法》一文中,我們提到:可以將異常單獨地封裝為一個方法,從而讓一個方法中只關注一件事,所以我們可以調整如下:
try{
tryDoSomething();
} catch (AStatusException e) {
e.printStackTrace();
} catch (BStatusException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
...
private void tryDoSomething(){
A a = getA();
B b = a.getB();
doSomething(b);
}
這樣調整之后,我們就可以將處理異常的部分,單獨剝離出來。這樣,如果我們只關心正常業務邏輯,就只關心tryDoSomething()就可以了。
封裝異常
觀察上一小節中的代碼,我們可以發現對于tryDoSomething()的方法,我們進行了兩種指定類型的catch操作,但是在當前代碼中,這兩個類型的catch的處理動作邏輯并沒有差別,也就是說當前的業務邏輯并不關心異常的類型。而進一步地說,異常的類型實際上是底層方法的實現細節。所以,就會出現一個相互排斥的問題:
底層實現(或者公共API)希望提供足夠信息的異常場景信息。
上層實現并不對所有的異常信息關心。
針對這兩種情況,我們可以通過使用異常進行封裝的方法來對下層的異常進行抽象,從而對上層調用屏蔽細節,方法如下:
try{
tryDoSomethingWithSameException();
} catch (StatusException e) {
e.printStackTrace();
}
...
private void tryDoSomethingWithSameException(){
try{
tryDoSomething();
} catch (AStatusException e) {
throw new StatusException(e);
} catch (BStatusException e) {
throw new StatusException(e);
} catch (Exception e) {
throw new StatusException(e);
}
}
tryDoSomethingWithSameException()這個方法可能是在一個單獨的代理類中定義的,或者是通過其他方式定義的,但是總的來說,通過使用tryDoSomethingWithSameException()方法,我們在最外層實際調用的時候就只用關心StatusException的這個方法就可以了。
同時,由于使用了
tryDoSomethingWithSameException() 方法,如果當我們調整tryDoSomething();中的業務邏輯而產生新的異常的時候,我們就不需要調整主業務邏輯的文件了,而只用調整異常封裝類就可以了,就讓我們可以更少的修改業務主流程。
非受檢異常
你會發現,在上文的方法中無論是tryDoSomething(),還是
tryDoSomethingWithSameException() 我們都沒有使用 throw。也就是說,我們使用的是“非受檢異常”。那么如果如果我們使用“受檢異常”會怎么樣呢,代碼會變成這樣:
private void tryDoSomething() throws AStatusException, BStatusException {
A a = getA();
B b = a.getB();
doSomething(b);
}
在方法上需要對方法內拋出的異常進行定義。或許有的人認為這種方式十分好,因為足夠明確,一眼就知道會出現什么異常。并且在上層進行使用的時候我們也可以直觀地知道方法可能出現的異常。
但是這種方式的優點也同樣成為了缺點,因為異常的描述直接變成了方法簽名中的一部分。而且由于是受檢異常,所以會逐級地向上傳遞,直到上層那里進行了捕獲處理。也就是說,如果不想在當前方法中處理異常的話,就要將異常添加到方法簽名上。從而使得調整一個底層邏輯新增一個異常的時候,會導致所有調用該方法的方法都需要進行調整,而這顯然是不符合開閉原則的。
當然,對于一下關鍵的邏輯,你可能會需要讓開發人員明確地知道可能會存在的異常。但是對于更多的一般情況,非受檢異常的使用,會更適合代碼的可維護性。
特殊對象代替異常
當我們嘗試獲取一個列表的時候,可能會使用如下的方法:
List<File> files = getFileByPath("xxx");
我們會通過getFileByPath()嘗試獲取文件列表。那么針對“xxx”的這個變量,如果他不是一個有效的路徑,那就有可能存在異常邏輯。我們可以通過拋出一個"FileNotFoundException"的異常來描述這種的異常情況,但這就需要上層對異常邏輯進行處理,這回導致增加額外的邏輯。
所以,當上層對于下層的異常不敏感的時候,我們可以調整數據的返回值,讓他成為一種不會影響業務邏輯的特別返回值,從而減少整體的業務維護代碼。
以本節的例子舉例,就是當異常的時候,在方法內部進行捕獲,然后使用"Collections.emptyList()"返回一個空的列表。這樣后續的處理邏輯就可以正常執行。當然要保證這樣的處理和你的業務是吻合的。
出入不歡迎null
不要用null!不要用null!不要用null!
不論出入都不用用null作為入參或者出參。原因很簡單,一旦你的代碼中中出現了返回null的代碼風格,那么你所有的處理邏輯中都要對null做出判斷。即便java可以使用Optional簡化連續為空的處理,但這是給自己增加沒有必要的工作量。假如本文中第一個例子的返回值可能為空,那么這個代碼就是有問題的,因為幾個判斷里面都可能會拋出"NullPointerException"而本方法中沒有捕獲,上層要是也沒有的話,這次邏輯執行就會直接以失敗告終。但是如果對null進行判斷,代碼就會變成如下:
A a = getA();
if(a != null && a.status != xxx ){
B b = a.getB();
if(b != null && b.status != xxx ){
doSomething(b);
}
但起來沒有多少增加的邏輯,但是要知道當所有的判斷中都需要處理這個情況的時候,就很可怕了。更可怕的是如果你忘記了其中的一個判斷,代碼就會在下一次的時候從不知道哪里拋出一個"NullPointerException"。
所以,為了減少不必要的業務邏輯維護,不要讓null成為你“正常邏輯”中的一種返回。
最后
在進行業務編碼的過程中,我們不可避免地需要處理正常邏輯之外的異常邏輯。但如果不處理好異常邏輯,那么異常邏輯的維護就會侵占正常邏輯的位置,讓系統整體的理解成本增高。所以,優雅的處理異常可以讓系統的可維護性大大提高。