# 虎年行大運 ~ Clean Code 系列 - 編排 ## 【前言】 最近開始看 Clean Code 了,接下來會做關於書中內容的心得與個人經驗整理的分享文。 ## 【主文】 藝術這種東西、或稱概念,真的是見仁見智。 會用藝術來表達編排,也就表明了這是一種不被硬性規定的行為,如果你願意遵循,那或多或少可以幫助你在程式碼的設計架構上更清楚鮮明。當然這也不代表只有一種模式可以遵循,因應不同團隊、環境,有不同的方式需要遵循。 至於沒有這種規範的團隊或環境,則很適合去起個頭著手整頓一下大家的風格與意見,從而擬定建構出屬於你們的一套 Coding Style 靈活的運用可以清晰整體程式結構,幫助開發者在閱讀程式碼的速度與細節上的理解更快更準確,Kai 下面會整理一些書中的道理與自身碰到的例子做說明~ ## 【書中的大道至簡】 - **垂直** - **概念間的換行區隔** 現行的 IDE 幾乎都是從左至右,從上至下的書寫方式,這也是最貼近**自然視覺 - F 型瀏覽模式**的方式,至於什麼是 **F 型瀏覽模式**,可參考該篇文章 - [F型瀏覽模式:設計一個良好的視覺層級結構](https://kknews.cc/zh-tw/design/mmlln4g.html)。 如果讓程式碼的上下全部緊貼,那麼將會很難在短時間內辨識說諸如: 套件管理、建構子、屬性、方法等區塊內容,甚至也會不好理解程式全貌。 ```java= // 這是一段 Kai 在寫 LeetCode 的範例,壓成無垂直分段的樣子 import java.util.ArrayList; import java.util.List; public class MedianOfTwoSortedArray { public static void main(String [] args){ Solutions solutions = new Solutions(); int [] nums1 = new int[]{1,2,3}; int [] nums2 = new int[]{4,5,6}; System.out.println(solutions.findMedianSortedArrays(nums1, nums2)); } } class Solution { ... } ``` ```java= // 再看一段,有垂直分段的樣子 import java.util.ArrayList; import java.util.List; public class MedianOfTwoSortedArray { public static void main(String [] args){ Solutions solutions = new Solutions(); int [] nums1 = new int[]{1,2,3}; int [] nums2 = new int[]{4,5,6}; System.out.println(solutions.findMedianSortedArrays(nums1, nums2)); } } class Solution { ... } ``` 上述兩段程式碼是不是讓你有感閱讀所花費的時間不一樣呢? Kai 希望有^^ - **密度與距離** 相同的概念,盡量放置的靠近。 相依的概念,盡量放置在上下,對應使用關係上。 這是為了讓彼此閱讀起來更有連貫性、關係性,不要讓開發者花太多的時間追尋那些不應該需要重複閱讀的過程。 ```java= // 看一段被區隔開來的參數 public class Config{ public Config(){} /** * the class name of the config */ private String name; /** * the value of the config */ private String value; public Config(String name, Stirng value){ this.name = name; this.value = value; } } ``` ```java= // 再看一段整理過的 public class Config{ private String name; // class name of the config private String value; // value of the config public Config(){} public Config(String name, Stirng value){ this.name = name; this.value = value; } } ``` 上述並不是個太好的例子,因為這種註解基本上不該存在,但應該能夠董相同的概念需要放置一起這句話。至於相依的概念部分則是可以用以下舉例 ```java= // 看起來還不難看對吧? 但應該還可以再梳理一下~ public class Configs{ public Configs(List<String> names, List<Stirng> values){ this.setConfig(names, values); checkConfigValid(); sendFailureResponse(); } checkEachConfigValid(){...} sendFailureResponse(){...} setConfig(List<String> names, List<Stirng> values){...} checkConfigValid(){ for(...){ checkEachConfigValid() } } } ``` ```java= // 看起來"有序"多了對吧? public class Configs{ public Configs(List<String> names, List<Stirng> values){ this.setConfig(names, valus); checkConfigValid(); sendFailureResponse(); } setConfig(List<String> names, List<Stirng> values){...} checkConfigValid(){ for(...){ checkEachConfigValid() } } checkEachConfigValid(){...} sendFailureResponse(){...} } ``` 因 **checkConfigValid()** 呼叫了 **checkEachConfigValid()** 做使用,因此在編排上兩者應該盡可能靠攏,且應為 **checkConfigValid()** 放在上方;**checkEachConfigValid()** 在下方的方式。其他方法就依照在建構子中被呼叫的順序而定,這樣讓開發者逐一順下來的時候會更能從方法的次序去了解整體被呼叫時候的位置。 - **變數的宣告** 通常會將變數放置於最靠近使用的地方,若是實體變數(Instance Variables)則會放置在類別靠近上方的位置,因為他可能會被大多數的方法使用到。 若方法本身行數不多的時候,會建議將變數放在整體接近上方的部分,這是因為若方法有輸入的參數,讓開發者第一時間認知到有哪些輸入參數?內部有哪些變數?可以直接加深印象,繼續研讀的時候也不需要再去記得其他參數。 而程式碼本身很長的時候,變數的宣告則盡量靠近被使用到的部分,開發者在分段閱讀的時候可以容易追溯到變數在哪裡這件事情。 - **水平** - **總寬** 從以往的窄螢幕發展到現在的寬螢幕,一個螢幕可以容納的字元變得更多,開發者在編寫程式碼的時候更無須注意換行的需求。但事實上真是這樣嗎? 據統計來說,單行寬度字元達到 80 ~ 90 時,使用者習慣人數便會呈現雪崩式下滑,許多工程師反映無邊際的水平擴展程式碼完全不是朝著高可維護性的方向前進。 對大多數工程師而言,習慣的是介於 20 ~ 60 個字元之間,當然這完全都沒有任何硬性規定存在,如果能夠有建議,不妨測量一下公司提供的螢幕寬度,然後以螢幕寬度來制定最適當。畢竟你跟你的團隊都在同一間公司,使用的應當是同一類型的螢幕才對。 - **空白** 這邊提的不是為了縮排的空白,而是字裡行間的空白處理。這是為了凸顯出行段間的個別重要部分而做的事情,甚至在另一種用途上,空白還代表了強調運算子的優先權 (只是這部分通常會被自動化編排工具當成一般空白給忽略掉...),而這種運用手法尤其適合用在數學運算上。 ```java= // 舉個例子: 計算一個梯形面積並且減掉一個已知面積的內部圖形 // 未整理的 public double countArea(int topWidth, int downWidth, int height, double area2){ return (topWidth+downWidth)*height/2-area2; } // 整理後的 public double countArea(int topWidth, int downWidth, int height) { return (topWidth + downWidth) *height/2 - area2; } ``` 你完全可以明顯感受到整個公式變得更有訴說力,他們在強調著先後的邏輯順序,告知你整體的運算流程。眼尖的人甚至可以發現以下的不同。 ```java= countArea(int topWidth, int downWidth, int height, double area2){} countArea(int topWidth, int downWidth, int height, double area2) {} ``` 這是為了強調方法與參數的關係,使其不與實作區搞混。但這種作法實際上也沒有太要求就是了。 - **對齊** 有不少工程師喜歡做以下事情,去對齊他們的參數或是方法等等。 書中的作者原先也是喜歡採用這種方式撰寫程式碼,但久而久之發現,這反而強調了更多不應該去注意的部分,好比你看著 **getValue1**,同時也注意到了下面兩個參數,朦朧間你就認定了下面也是 private String 的宣告與型態,但這明顯是不對的。 你的視覺在不經意的時候已經注意到了不須要注意的部分,也容易因連續而造成錯誤的認知,更重要的是自動編排工具會完全否定這種格式,因此這樣的寫法不一定好用,有時壞處可能更大一些。 而且當你的參數或是方法列表很長的時候,通常就意味著設計架構需要被好好的檢討了。 ```java= private String getValue1; private int getValue2; protected boolean getValue3; ``` - **縮排** 為了更有力的將區塊之間的不同表達出來,除了透過垂直的分隔以外,還可以再透過水平的縮排進行處理。 Kai 就直接拿第一段的舉例繼續處理。 ```java= // 這是一段 Kai 在寫 LeetCode 的範例,壓成無垂直分段、無水平縮排的樣子 import java.util.ArrayList; import java.util.List; public class MedianOfTwoSortedArray { public MedianOfTwoSortedArray(){System.out.println("Hello!");} public static void main(String [] args){ Solutions solutions = new Solutions(); int [] nums1 = new int[]{1,2,3}; int [] nums2 = new int[]{4,5,6}; System.out.println(solutions.findMedianSortedArrays(nums1, nums2)); } } class Solution { ... } ``` ```java= // 再看一段,有垂直分段、無水平縮排的樣子 import java.util.ArrayList; import java.util.List; public class MedianOfTwoSortedArray { public MedianOfTwoSortedArray(){System.out.println("Hello!");} public static void main(String [] args){ Solutions solutions = new Solutions(); int [] nums1 = new int[]{1,2,3}; int [] nums2 = new int[]{4,5,6}; System.out.println(solutions.findMedianSortedArrays(nums1, nums2)); } } class Solution { ... } ``` ```java= // 最後,有垂直分段、有水平縮排的樣子 import java.util.ArrayList; import java.util.List; public class MedianOfTwoSortedArray { public MedianOfTwoSortedArray(){ System.out.println("Hello!"); } public static void main(String [] args){ Solutions solutions = new Solutions(); int [] nums1 = new int[]{1,2,3}; int [] nums2 = new int[]{4,5,6}; System.out.println(solutions.findMedianSortedArrays(nums1, nums2)); } } class Solution { ... } ``` 相信你完全可以透過第三段的程式碼更容易理解區塊的範圍,縮排不僅是為了給大區塊提供明確的範圍,諸如像是 if/else、while()、甚至是只有一行陳述句的方法,都應該做到垂直加水平整頓,甚至是雖然 if 允許陳述句只有一句便可不加括號,但還是請乖乖的換一行、縮一排、加上括號處理,你不會想要哪一天這個結構被誰在下面加了一句新的陳述句卻因永遠啟動而炸鍋的。 - **空視野** 類似 if,while 也有不使用陳述句的狀況,這種時候會很容易混淆開發者的閱讀,畢竟那不算是能直覺得理解的部分。因此最好能避免這種寫法就避免,不能避免的話,也請加個分號加以提示。 你總是會想針對只有一行的處理偷個懶,但其實寫的正規一點並不會花你多少時間,甚至可以避免未來發生錯誤的可能。 ```java= // 你不會想遇到哪一天誰在下面加了一段新 CODE if(true) System.out.println("Hello!"); System.out.println("world"); // While 在 readBuffer 的處理很常被這樣用於隨時監聽回應 while(dis.read(buf, 0, readBufferSize) != -1) ``` ```java= if(true) { System.out.println("Hello!"); } System.out.println("world"); while(dis.read(buf, 0, readBufferSize) != -1){} // or while(dis.read(buf, 0, readBufferSize) != -1) ; ``` ## 【結語】 編排算是針對 Coding Style 講述非常多的部分,而這些從來不是硬性規定,書中的最後提到了需要與你的開發團隊協商,共同討論出一個大家都願意接受的風格並進行推動。 能夠逐步一致化產品間的程式碼設計結構,彼此在往後互相支援時也不會有太多的詫異感。 首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA) ###### tags: `Clean Code 無瑕的程式碼`