<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Clean Code - 錯(cuò)誤處理

          共 7443字,需瀏覽 15分鐘

           ·

          2022-06-11 21:32

          當(dāng)錯(cuò)誤發(fā)生時(shí),程序員有責(zé)任確保代碼照常工作

          錯(cuò)誤處理很重要,但如果它搞亂了代碼,就是錯(cuò)誤的做法,接下來的內(nèi)容將會(huì)談及如何優(yōu)雅地進(jìn)行代碼的錯(cuò)誤處理。

          1、使用異常而非返回碼

          反例

          public?class?DeviceController?{?...
          ????public?void?sendShutDown()?{?
          ????????DeviceHandle?handle?=?getHandle(DEV1);?//?Check?the?state?of?the?device
          ????????if?(handle?!=?DeviceHandle.INVALID)?{
          ????????????//?Save?the?device?status?to?the?record?field?
          ????????????retrieveDeviceRecord(handle);
          ????????????//?If?not?suspended,?shut?down
          ????????????if?(record.getStatus()?!=?DEVICE_SUSPENDED)?{
          ????????????????pauseDevice(handle);?
          ????????????????clearDeviceWorkQueue(handle);?
          ????????????????closeDevice(handle);?
          ????????????}?else?{
          ????????????????logger.log("Device?suspended.?Unable?to?shut?down");
          ????????????}
          ????????}?else?{
          ????????????logger.log("Invalid?handle?for:?"?+?DEV1.toString());?
          ????????}
          ????}
          ...?
          }

          使用返回碼的問題在于,他們搞亂了調(diào)用者代碼。對(duì)于使用返回碼的函數(shù),調(diào)用者在得到返回碼之后要立即使用 if 語句去驗(yàn)證返回碼不幸的是,這個(gè)步驟很容易被遺忘。

          此外,使用返回碼的話,業(yè)務(wù)邏輯和錯(cuò)誤處理代碼耦合在一起這對(duì)于代碼的可讀性和結(jié)構(gòu)的合理性都是極大的挑戰(zhàn)。

          所以,遇到錯(cuò)誤時(shí),最好的辦法是拋一個(gè)異常。當(dāng)程序出現(xiàn)錯(cuò)誤時(shí),調(diào)用者能夠立即接收到這個(gè)異常,無需調(diào)用者去判斷。

          此外,使用異常處理能讓業(yè)務(wù)邏輯和錯(cuò)誤處理在代碼結(jié)構(gòu)上分離,調(diào)用代碼很整潔,其邏輯不會(huì)被錯(cuò)誤處理搞亂。

          正例

          public?class?DeviceController?{?
          ????...
          ????public?void?sendShutDown()?{?
          ????????try?{
          ????????????tryToShutDown();
          ????????}?catch?(DeviceShutDownError?e)?{
          ????????????logger.log(e);?
          ????????}
          ????}
          ????private?void?tryToShutDown()?throws?DeviceShutDownError?{?
          ????????DeviceHandle?handle?=?getHandle(DEV1);
          ????????DeviceRecord?record?=?retrieveDeviceRecord(handle);
          ????????pauseDevice(handle);?
          ????????clearDeviceWorkQueue(handle);?
          ????????closeDevice(handle);
          ????}
          ????private?DeviceHandle?getHandle(DeviceID?id)?{?
          ????????...
          ????????throw?new?DeviceShutDownError("Invalid?handle?for:?"?+?id.toString());
          ????????...?
          ????}
          ...?
          }

          見上述代碼,業(yè)務(wù)邏輯與錯(cuò)誤處理部分截然分開,錯(cuò)誤處理部分被隔離到 Exception 的子類 DeviceShutDownError 中處理了。

          2、先寫 Try-Catch-Finally 語句

          當(dāng)遇到需要做異常處理的時(shí)候,首先把try-catch-finally塊寫出來,這能幫助你寫出更好的異常處理代碼。

          3、使用不可控異常

          可控異常(也叫可檢查異常,checked exception):
          這類異常是可以預(yù)測(cè)的,我們必須在程序中做處理,try...catch 捕獲或者 throw 拋出。比如 IOException(網(wǎng)絡(luò)異常)、SQLException(SQL異常) 等等。

          不可控異常(也叫不可檢查異常,unchecked exception,也叫運(yùn)行時(shí)異常):
          這類異常是程序運(yùn)行時(shí)發(fā)生的,是無法預(yù)測(cè)的。比如 NullPointerException(空指針異常)、ClassCastException(類型轉(zhuǎn)換異常)、 IndexOutOfBoundsException(數(shù)組越界異常)等等。

          如果在某些特殊的情況下必須要捕獲異常并作出處理,那么不得不使用可控異常。但是在通常的開發(fā)過程中應(yīng)當(dāng)避免使用可控異常。

          原因在于可控異常其違反了開放-封閉原則。如果你在方法中拋出可控異常,而 catch 語句在三個(gè)層級(jí)之上,你就得在 catch 語句和拋出異常處之間的每個(gè)方法簽名中聲明該異常。這意味著對(duì)軟件中較低層級(jí)的修改,都將波及到較高層級(jí)的修改。修改好的模塊必須重新構(gòu)建、發(fā)布,即便它們自身所關(guān)注的任何東西都沒有修改過

          以某個(gè)大型系統(tǒng)的調(diào)用層級(jí)為例。頂端函數(shù)調(diào)用它們之下的函數(shù),逐級(jí)向下。假設(shè)某個(gè)位于最底層的函數(shù)被修改為拋出一個(gè)異常,如果這個(gè)異常是可控異常,則函數(shù)簽名就要添加 throws 子句。這意味著每個(gè)調(diào)用該函數(shù)的函數(shù)都要修改,捕獲新異常,或者在其簽名中添加 throws 子句,以此類推,最終得到的就是一個(gè)從軟件最底端貫穿到最高端的修改鏈!

          示例 1(最底層函數(shù)不拋異常):

          public?void?function1(){
          ??function2();
          }

          public?void?function2(){
          ??function3();
          }

          public?void?function3(){
          ??function4();
          }

          public?void?function4(){
          ??/*...*/
          }

          示例 2(底層函數(shù)拋出一個(gè)可控異常):

          public?void?function1(){
          ??try{
          ????function2();
          ??}catch(IOException?exception){
          ????logger.info("function1?出現(xiàn)異常",?exception);
          ??}
          }

          public?void?function2()?throws?IOException{
          ??function3();
          }

          public?void?function3()?throws?IOException{
          ??function4();
          }

          public?void?function4()?throws?IOException{
          ??/*...*/
          }

          底層函數(shù) function4() 拋出一個(gè)可控異常 IOException。然后 function3() 調(diào)用 function4(),那么 function3() 要么 try catch 處理這個(gè)異常,要么不處理拋給上一層,我們這里選擇拋出異常 IOException。再然后 function2() 調(diào)用 function3() ,那么 function2() 也拋出異常 IOException。最后最上層函數(shù) function1() 調(diào)用 function2(),由于 function1() 是最上層函數(shù),所以,我們采用 try catch 的方式,捕獲異常并處理異常。

          由于底層函數(shù)的修改,導(dǎo)致整個(gè)函數(shù)調(diào)用鏈路的修改,這明顯違反了開閉原則。

          4、給出異常發(fā)生的環(huán)境說明

          拋出的每個(gè)異常,都應(yīng)當(dāng)提供足夠的環(huán)境說明,以便判斷錯(cuò)誤的來源和處所,即錯(cuò)誤信息應(yīng)當(dāng)充分,讓追蹤調(diào)用棧的排查者更容易查找到錯(cuò)誤原因。

          5、依調(diào)用者的需求定義異常類

          對(duì)錯(cuò)誤分類有很多方式。可以依其來源分類:是來自組件還是其他地方?或依其類型分類:是設(shè)備錯(cuò)誤、網(wǎng)絡(luò)錯(cuò)誤還是編程錯(cuò)誤?不過,當(dāng)我們?cè)趹?yīng)用程序中定義異常類時(shí),最重要的考慮應(yīng)該時(shí)它們?nèi)绾伪猾@取。

          反例

          下面的 try catch finally 語句是對(duì)某個(gè)第三方 API 的調(diào)用,我們把調(diào)用可能拋出的異常都列了出來。我們可以看到,語句中包含了大量的重復(fù)代碼(一長(zhǎng)串的 catch(){/.../} )。

          ACMEPort?port?=?new?ACMEPort(12);
          try?{
          ??port.open();
          }catch(DeviceResponseException?e){
          ??reportPortError(e);
          ??logger.log("Device?response?exception",?e);
          }catch(ATM1212UnlockedException?e){
          ??reportPortError(e);
          ??logger.log("Unlock?exception",?e);
          }catch(GMXError?e){
          ??reportPortError(e);
          ??logger.log("Device?response?exception");
          }finally{
          ??/*...*/
          }

          正例

          我們定義一個(gè)通用異常類型 PortDeviceFailure,以及一個(gè)打包類 LocalPort,然后將上述的 API 調(diào)用以及異常處理代碼封裝到這個(gè)打包類中,最后讓打包類返回通用異常類型,從而簡(jiǎn)化代碼。

          LocalPort?port?=?new?LocalPort(12);
          try{
          ??port.open();
          }catch(PortDeviceFailure?e){//?捕獲通用異常類型
          ??reportError(e);
          ??logger.log(e.getMessage(),?e);
          }finally{
          ??/*...*/
          }

          //?定義一個(gè)通用異常類型
          public?class?PortDeviceFailure?extends?Exception{
          }

          //?定義一個(gè)打包類
          public?class?LocalPort{
          ??private?ACMEPort?innerPort;
          ??public?LocalPort(int?portNumber){
          ????innerPort?=?new?ACMEPort(portNumber);
          ??}
          ??public?void?open(){
          ????try?{
          ??????innerPort.open();
          ????}catch(DeviceResponseException?e){
          ??????throw?new?PortDeviceFailure(e);//?讓打包類返回通用異常類型?
          ????}catch?(ATM1212UnlockedException?e){
          ??????throw?new?PortDeviceFailure(e);
          ????}catch(GMXError?e){
          ??????throw?new?PortDeviceFailure(e);
          ????}
          ??}
          ??/*...*/
          }

          在反例中,我們?cè)?catch 異常時(shí),把第三方可能拋出的異常都 catch 住了,整段代碼有很多重復(fù)的地方。經(jīng)過優(yōu)化,我們用一個(gè)通用的異常代替了這些異常,然后把具體的 API 調(diào)用以及異常處理代碼封裝到一個(gè)打包類中,通過這種方式可以大大地簡(jiǎn)化代碼。

          實(shí)際上,對(duì)第三方類庫中的 API 進(jìn)行封裝會(huì)帶來很多好處

          封裝的好處在于你可以不需要一定遵循這個(gè)類庫的設(shè)計(jì)來使用它,你可以定義自己感覺舒服的 API。在上例中,我們?yōu)?port 設(shè)備錯(cuò)誤定義了一個(gè)異常類型,然后發(fā)現(xiàn)這樣能寫出更整潔的代碼。

          6、定義常規(guī)流程

          雖然我們可以寫出很好的錯(cuò)誤處理代碼,它們外形優(yōu)雅、結(jié)構(gòu)清晰。但是錯(cuò)誤處理不能亂用,它只能用于以下這種情況--因?yàn)槌绦虺霈F(xiàn)錯(cuò)誤而想要終止代碼的操作,不能將它用于業(yè)務(wù)邏輯處理

          我們舉個(gè)栗子解釋一下,下面代碼來自某個(gè)記賬應(yīng)用的開支總計(jì)模塊。

          反例

          try{
          ??MealExpenses?expenses?=?expenseReportDAO.getMeals(employee.getID());
          ??m_total?+=?expenses.getTotal();
          }catch(MealExpensesNotFound?e){
          ??m_total?+=?getMealPerDiem();
          }

          上面這段代碼的業(yè)務(wù)邏輯是,如果消耗了餐食,則計(jì)入總額,如果沒有,則拋出 MealExpensesNotFound 異常,員工得到當(dāng)日餐食補(bǔ)貼。

          上述代碼中的異常是為了處理業(yè)務(wù)邏輯的一種情況--員工沒有消耗餐食,而不是要中止計(jì)算而拋出異常,這是不規(guī)范的,它可以被重構(gòu)為如下的樣子。

          可以修改 ExpenseReportDAO,使其總是返回 MealExpenses 對(duì)象。如果沒有餐食消耗,就返回一個(gè)返回餐食補(bǔ)貼的 MealExpenses 對(duì)象。

          正例

          MealExpenses?expenses?=?expenseReportDAO.getMeals(employee.getID());
          m_total?+=?expenses.getTotal();

          //這里引入一個(gè)新的特殊情況的類
          public?class?PerDiemMealExpenses?implements?MealExpenses?{
          ??public?int?getTotal()?{
          ????//?return?the?per?diem?default
          ??}
          }

          這種編程模式被叫做特例模式(也叫特殊情況模式),它創(chuàng)建一個(gè)新的類或配置一個(gè)對(duì)象,來處理這種特殊情況。

          7、別返回 null 值

          null 是邪惡的,不要在代碼中返回 null 值。

          如果你的代碼有返回 null 值的情況,那么對(duì)于每一個(gè)可能為 null 的對(duì)象,都要對(duì)它進(jìn)行 null 判斷,否則就會(huì)拋出空指針異常,見如下代碼。

          public?void?registerItem(Item?item){?
          ??if(item?!=?null){//?item?是一個(gè)對(duì)象,需要做?null?判斷,否則會(huì)出現(xiàn)空指針異常
          ????ItemRegistry?registry?=?peristentStore.getItemRegistry();?
          ??????if(registry?!=?null){//?registry?是一個(gè)對(duì)象,需要做?null?判斷,否則會(huì)出現(xiàn)空指針異常
          ????????Item?existing?=?registry.getItem(item.getID());
          ??????????if(existing.getBillingPeriod().hasRetailOwner()){
          ????????????existing.register(item);?
          ??????????}
          ??????}
          ??}
          }

          這就會(huì)造成以下幾個(gè)問題:

          (1)增加自己的工作量,你的代碼中需要有一大堆的判斷一個(gè)對(duì)象是否為 null 的代碼。

          (2)如果疏忽了,只要有一處沒有檢查 null 值,應(yīng)用程序就會(huì)失控。

          可以使用以下幾種方法,來避免返回 null。

          • 拋出異常
          • 返回特例對(duì)象,即永遠(yuǎn)返回一個(gè)有值的對(duì)象。
          • 如果你在調(diào)用某個(gè)第三方 API 中可能返回 null 值的方法,可以考慮用新方法打包這個(gè)方法,在新方法中拋出異常或返回特例對(duì)象。

          下面我們用一個(gè)例子,詳述如何通過返回特例對(duì)象,來避免返回 null 值。

          反例:

          public?List?getEmployees(){
          ??if(..there?are?no?employees?..){
          ????return?null;
          ??}
          }

          List?employees?=?getEmployees();?
          if(employees?!=?null){//?對(duì)?getEmployees()?的返回值做?null?判斷
          ??for(Employee?e?:?employees){?
          ????totalPay?+=?e.getPay();
          ??}?
          }

          在這個(gè)例子中,在 getEmployees() 方法中,當(dāng)沒有員工時(shí),返回 null 值。那么當(dāng)我們使用 getEmployees() 方法時(shí),就必須對(duì)它的返回值做 null 判斷,否則會(huì)出現(xiàn)空指針異常。

          正例:

          public?List?getEmployees(){
          ??if(..there?are?no?employees?..){
          ????return?Collections.emptyList();
          ??}
          }

          List?employees?=?getEmployees();?
          for(Employee?e?:?employees){?
          ??totalPay?+=?e.getPay();
          }?

          在這個(gè)例子中,在 getEmployees() 方法中,當(dāng)沒有員工時(shí),返回一個(gè)空列表,此時(shí)就不需要 null 判斷了,代碼變得整潔多了。

          8、別傳遞 null 值

          在方法中返回 null 值是糟糕的做法,但將 null 值傳遞給其他方法就更糟糕了。除非 API 要求你向它傳遞 null,否則禁止傳遞 null 值

          反例:

          public?class?MetricsCalculator{
          ??public?double?xProjection(Point?p1,?Point?p2){
          ????return?(p2.x?-?p1.x)?*?1.5;
          ??}
          }

          //?調(diào)用?xProjection()?方法時(shí),第一個(gè)參數(shù)傳入?null
          calculator.xProjection(null,?new?Point(12,?13));

          9、總結(jié)

          這一章主要做的就是讓錯(cuò)誤處理不影響代碼可讀性,并且利用錯(cuò)誤處理加強(qiáng)代碼魯棒性,讓二者不沖突。


          瀏覽 81
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  内射操逼 | 人人射人人 | 啪啪啪啪啪啪网站 | 日本肉丝袜三级片在线免费看 | 北条麻妃一区二区三区成人片 |