--- title: Clean Code 無暇的程式碼 tags: Coding Skill --- # Clean Code 無暇的程式碼 ![](https://i.imgur.com/wCHloJ9.jpg) `WHY` : 程式很多人都會寫,故事卻很少人會寫,如果能把程式寫成故事,那麼閱讀程式的人也會感到興奮。 [TOC] --- ## 第一章-有意義的命名 ### 類別的命名 類別的命名應該使用名詞或名詞片語來命名 ```java= public class customer{ ... } public class wikiPage{ ... } public class account{ ... } public class addressPaser{ ... } ``` :::warning 類別的名稱不應該是動詞 ::: ### 方法的命名 方法應該使用動詞或動詞片語來命名 ```java= public static void setName(string _name){ ... } string name = employee.getName(); customer.setName("leo"); if(leo.isPaid())... ``` > 根據JavaBean的標準,取出器(取出的方法)應該使用Get當字首、修改器(設定的方法)應該使用Set當字首,判定(predicate)應該使用is當字首。 ### 每個概念使用一種字詞 在同一個函數庫(library)裡,如果分別使用Controller、Manager和Driver,**這三個類別混雜在一起會使人混亂**。除非你能分辨DeviceManager(裝置管理員)與ProtocolController(協定控制器)實質的差別是什麼,不然就全部改用單一種字詞來命名就好。 ```java= //錯誤示範 相似觀念用一種類別來撰寫就好 public class controller{ ... } public class manager{ ... } public class driver{ ... } ``` :::success 全部都改用Controller或Manager這兩個都可以代表Driver的意思。 ::: ```java= // 用一種類別來改寫即可 public class controler{ ... } ``` ### 使用解決方案領域的名稱 閱讀你程式的人也會是程式設計師,所以盡量使用資訊領域的專業術語來進行命名,如`演算法`、`模型名稱`、`數學詞彙` ```java= public static string BankersAlgorithm(){ while (P != ∅) { found = FALSE; foreach (p ∈ P) { if (Mp − Cp ≤ A) { /* p可以獲得他所需的資源。假設他得到資源後執行;執行終止,並釋放所擁有的資源。*/ A = A + Cp ; P = P − {p}; found = TRUE; } } if (! found) return FAIL; } return OK; } // 作業系統-Banker演算法 拿來預防Deadlock BankersAlgorithm(); ``` ### 添加有意義的上下文 想像著你有一些變數,命名為firstName、lastName、city、state,放在一起看的話,你很清楚這是在記錄一份地址。 ```java= // 你很清楚這是一份地址與個人資料 String firstName, lastName, city, state; ``` 但如果你在方法裡只單純看到state這個變數你還會推測她是地址的一部分嗎 :stuck_out_tongue:? ```java= // 如果只有state 你還清楚嗎? string state; ``` 可以利用字首來增加上下文資訊 ```java= // 如果這樣宣告的話不是更清楚嗎? string addrState, addrFirstName; ``` :::success 最好的方式是,產生一個名為Address的類別,讓這些變數隸屬於該類別,至少讀者會了解這些變數是來自更大結構的一部分。 ::: --- ## 第二章-函數 ### 只做一件事 If、Else、While都應該只有一行,而那行通常是呼叫Function來回傳T or F。 ```java= //錯誤示範 if((employee.flags & hourly_flag) && (employee.age > 65)) //正確 if(employee.isEligibleForFullBenefits()) ``` :::success **確保你的函式只做一件事,且把那件事做好** 確認只做一件事的最好方法就是,檢查可不可以從該Function中提取新的Function出來。 ::: ### 降層準則 程式的閱讀應該是由上而下的敘事,而不是東跳西跳。 ```java= public void function newDayInMyLife(){ wakeUp(); brushTooth(); haveBreakfast(); ... } public void function wakeUp(){ printf("Good morning~"); } public void function brushTooth(){ printf("Brushing my tooth~"); } public void function haveBreakfast(){ printf("Today I want to eat toast ^_^"); } ``` ### 輸出型的參數 以下函式真的是s接在某個東西的後方嗎?還是這個函式會把某些頁尾加到s後面? ```java= appendFooter(s); //我是輸出型函式 ``` 當我們回頭去看原函式的內容時會中斷我們閱讀上的思考。在物件導向的程式語言中,應該更改為以下方式來呼叫。 ```java= content.appendFooter(); ``` ### 函式的參數 **函式的參數數量最理想的是零個,盡量避免用多於三個參數。** ```java= public static void ideal(){ ... } ``` :::warning 參數的傳遞順序有多種組合,因此盡量避免用過多的參數。 輸出型的參數比輸入型更難以理解,當我們閱讀一個函式時,我們習慣於->**參數是輸入到函式**的概念,而**輸出則是透過回傳值來傳遞**,我們無法預期回傳的資訊由參數(輸入的值)來傳遞。 ::: ```java= public static void draw(float x, float y, color R, color G, color B, color Alphabet){ //這麼多參數 你只要給錯順序就會出錯 } ``` ### 要無副作用 :::warning 確保你的函式只做一件事,時常你所撰寫的函式**暗地裡**偷偷做了其他事情。有時候會使得同類別的其他變數,產生不可預期的改變。有時候他會將資訊轉換成參數傳遞給其他函式,或是轉變成系統的全域變數。 ::: ### 函式的詞性 Function通常使用`動詞`進行命名,代表函式所要進行的動作,而所接收的`參數以名詞`命名。 ```java= public void write(String name){ ...; } ``` ### 指令和查詢分離 函式最好一次僅做一件事情,例如回答某個問題或設定某個值,但這兩件事情不應該同時發生。看看以下的例子。 ```java= public boolean set(String attribute, String value); ``` :::warning 這個函式設定某個屬性的值,當設定成功則回傳Ture,設定失敗則回傳False代表該屬性不存在,這樣就會導致下面詭異的敘述。 ::: ```java= if(set("userName","leo"))... ``` 這行程式碼可以表達以下兩種意思 1. 設定userName的值為leo,判斷是否成功設定 2. 判斷userName的值是否為leo :::success 正確的方式應該要將查詢與設定的指令分開來撰寫 ::: ### 提取Try/Catch區塊 Try/Catch 區塊本身是難看的,在正常的程式運作中混入錯誤處理會混淆程式結構,比較好的做法是將函式從Try/Catch中提取出來。 :skull_and_crossbones: **錯誤示範** ```java= try{ deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); }catch (Exception e){ logger.log(e.getMessage()); } ``` :sunglasses: **正確寫法** ```java= public void delete(Page page){ try{ deletePageAndAllReferences(page); }catch (Exception e){ logError(e); } } private void deletePageAndAllReferences(Page page) throws Exception{ deletePage(page); registry.deleteReference(page.name); configKeys.deleteKey(page.name.makeKey()); } private void logError(Exception e){ logger.log(e.getMessage()); } ``` :::warning 如上述例子,函式應該只做一件事,而錯誤處理就是一件事,因此一個錯誤處理函式不應該再做其他事。關鍵字Try存在於一個函式裡它幾乎就代表該函式的開頭字眼,理應不該有其他程式。 ::: --- ## 第三章-註解 ### 用程式碼表達本意 ```java= //check to see if the employee is eliglible for full benefits if((employee.flags & hourly-flag) && (employee.age >65)) //改寫後(直接不需要註解) if(employee.isEligibleForFullBenefits()) ``` :::warning **註解的原則**:註解不該比Code長 ::: ### 可以寫的註解 * 法律型註解(條款 or 標準) * 資訊型註解 * 對意圖的解釋 * 闡明 * 對於後果的告誡 * 代辦事項(Todo) * 放大重要性 ### 法律型註解 *對於條款與標準的闡明* ```java= MIT ... ``` ### 資訊型註解 *對於傳入的參數進行解釋* ```java= // format matched kk:mm:ss EEE, MMM dd, yyyy Pattern timeMatcher = pattern.compile("\\d*;\\d*;...") ``` ### 對意圖的解釋 ```java= // This is our best attempt to get race condition // by creating large number of threads. ``` ### 闡明 有時候註解回傳可能是標準函式的一部分,或某些你不能修改的程式碼,加入註解可以降低查看原始碼的頻率 ```java= assertTrue(a.compareTo(a) == 0); // a == a assertTrue(aa.compareTo(ab) == -1); // aa < ab ``` ### 對於後果的告誡 有些程式我們並不曉得執行後會發生什麼事,然而這些事有時候並不如我們預期,例如:深度學習的訓練時間、執行緒安全問題...等 ```java= // 1. Don't run unless you have some time to kill // 2. SimpleDateFormat is not thread safe, // so we need to create each instance independetly. ``` ### 待辦事項(Todo) ```java= // Todo-MdM these are not needed // We expect this to go away when we do the checkout model. protected versionInfo makeVersion() throws Exception{} ``` ### 放大重要性 ```java= String listItemContent = match.group(3).trim(); // the trim is real important. It removes the starting // space that could cause the item to be recognized // as another list ``` ### 使用函式或變數來代替註解 ```java= // does the modeule from the global list <mod> depend on the // subsystem we are part of? if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem())) ``` ```java= // 改寫後 ArrayList moduleDependees = smodule.getDependSubsystems(); String ourSubSystem = subSysMod.getSubSystem(); if(moduleDependees.contains(ourSubSystem)){ ... } ``` ### 右大括弧( } )後的註解 我們時常會在右大括弧後撰寫括弧屬於誰的註解 ``` html= <div class="banner"> <div class="container"> <div class="banner-txt"> <h1>金魚都能懂的 <small>這個網頁畫面怎麼切</small> </h1> <h2>圖文滿版區塊</h2> <p>這畫面實在常見,在各種樣版網站可說是設計常客<br> 金魚切不出來實在說不過去阿</p> </div> </div> </div><!-- banner --> ``` :::warning **解決方式**:應試著縮短你的函式來解決 ::: ### 非區域性的Info 確保註解只用來描述範圍內的程式碼,不要跨區管理。 ``` java= /** * Port on which fitnesse would run. Defaults to <b>8082</b> * * @param fitnessePort */ public void setFitnessePort(int fitnessePort){ this.fitnessePort = fitnessePort; } ``` ### 不顯著的關聯 ```java= /* * 我每一天都想要這樣過 * 就是單純的吃飯睡覺 * 然後偶爾順便打東東 */ public void function newDayInMyLife(){ wakeUp(); brushTooth(); haveBreakfast(); ... } public void function wakeUp(){ printf("Good morning~"); } public void function brushTooth(){ printf("Brushing my tooth~"); } public void function haveBreakfast(){ printf("Today I want to eat toast ^_^"); } ```