0%

JAVA搞不清楚系列 - Clean Code 讀書心得


進到公司後被要求的第一件事就是看這本書
雖然在看的時候一直有種為什麼我在這?我在幹嘛?之類的感覺
但是沒想到這些東西會一直在之後做專案的時候冒出來
就連我自己都覺得很神奇
很推薦大家有空的話也可以看看他
雖然短期之內不會有立竿見影的效果
但我覺得書中的觀念會在不知不覺中影響著自己

Chapter 1 - 無瑕的程式碼

  1. 集中:不要在一段程式裡面想做一堆事
  2. 獨立:盡量減少依賴關係,以便於維護
  3. 可讀:讓人看得懂,設計成便於測試的
  4. 優化:盡可能的優化他,以免引得別人想來做優化而誤導致一堆混亂

命名應該名副其實,他會告訴你為什麼要存在、做什麼事、該怎麼用,如果你需要註解來補充就不算名符其實。

Chapter 2 - 有意義的命名

  1. 讓名稱代表意圖—使其名副其實

    代碼的簡潔度很重要,但是如果簡潔變成了模糊則沒有意義

  2. 避免誤導:拼寫與意圖相似的字詞
  3. 產生有意義的區別:如Data & Info幾乎沒差就選一個用就好
  4. 使用能唸出來的名稱:不要亂縮寫
  5. 使用可被搜尋的名字:命名的長度與其scope的大小相應
  6. 不要裝可愛:HolyHandGrenade ⇒ DeleteItem
  7. 每個概念使用一種字詞:fecth or retrieve or get
  8. 別說雙關語:add object or append list
  9. 使用解決方案領域的命名&使用問題領域的命名:技術性命名
  10. 添加有意義的上下文資訊:firstname+lastname+street+city = Address or define an object Address that IDE also can identify it.

Chapter 3 - 函式

每個函式都只做一件事,只包含一層抽象概念,從函式中無法再提煉出另外一個新函示。

  • 函式避免使用switch的敘
  • 理想的函數沒有參數—參數越多越不直觀、測試起來也越複雜,使用只有一個輸入型參數的函式是最好的做法
  • 輸出型的參數要避免使用:appendFooter(s) ⇒ report.appendFooter() //更明確的知道是加東西到哪裏

Chapter 4 - 註解

  • 註解是用來彌補失敗的程式碼,如果一段code無法清楚說明他的意圖而需要註解去輔助,則這不是一段成功的code。
  • 寫註解的動機往往是因為糟糕程式碼的存在,而註解只會越來越糟—因為工程師們在維護或是調整程式時容易忘記維護註解。
  • 與其花時間整理註解,不如整理糟糕的程式碼。

Chapter 5 - 編排

一、 垂直編排

  1. 概念間的垂直空白間隔:不同概念用空白間隔開來
  2. 垂直密度:相關程式碼應該更緊密
  3. 垂直距離:相近的概念不該被分散在不同檔案中
  4. 變數宣告盡可能靠近變數被宣告的地方
  5. 相依的函式在垂直編排上應該盡可能地靠近

概念相似性(Conceptual Affinity):程式裡的某些程式碼,希望能和其他程式碼盡可能地相近,因為他們在概念上有著相似的性質。

  1. 垂直的順序:被呼叫的函式應該出現在呼叫他的函式下方

二、 水平編排

  1. 空白使得左右分隔更突出
  2. 使用空白來強調運算子的優先權(沒空白的優先權越高)
  3. 使用縮排展現層級關係

Chapter 6 - 物件及資料結構

資料抽象化

抽象的介面:使用者不需要知道實現的過程,就能操縱資料

1
2
3
4
5
6
7
8
9
//具體化的交通工具類別
FuelTankCapacityInGallons(){
double getGallonsOfGasoline();
}

//抽象化的交通工具類別
public interface Vehicle{
double getPercentFuelRemaining();
}

我們不希望有任何人依賴這些變數。我們想保持一個自由的空間,這空間讓我們能自由地更改這些變數的型態,或是在出現突如其來的奇想或是衝動時,能自由地變更實現內容的程式碼。

The Law of Demeter (德摩特爾法則)

模組不該知道「關於他所操縱物件的內部運作」。物件將他們的資料隱藏起來,然後曝露其本身的操縱行為。

只和朋友說話,不和陌生人說話。

1
final String ouputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

上述程式碼被稱為train wreck (火車事故),在程式碼中要盡力避免,可以改寫成:

1
2
3
Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final String ouputDir = scratchDir.getAbsolutePath();

資料傳輸物件(Data Transfer Objects, DTO)

最佳的資料結構形式,是一個類別裡只有一個公用變數,沒有任何函式。

當我們要與資料庫溝通或是解析由socket傳來的訊息或諸如此類的事時,DTO在將「資料庫的原始資料」轉換成「應用程式內的物件」時,往往應用於轉換過程中的第一階段。

最常見的形式:Bean;Bean擁有可以被讀取、設定函式操作的私有變數(半封裝特性)

結論

  • 物件導向設計:有增加新資料型態的彈性
  • 資料型態和結構化的設計:有增加新行為的彈性

Chapter 7 - 錯誤處理

檢查型例外:Try-Catch-Finally

優點:try區塊就像是快交易處理(transaction)。不管在try裡發生了什麼意外,catch讓程式維持在一致的狀態。

缺點:使用檢查型例外的代價是違反了開放閉合準則(Open/Closed Principle)。如果對軟體中層次較低的函式進行修改,會迫使必須修改一連串高層次函式的署名。檢查型例外在相依性所花費的工夫比實質效益還要高上不少。

錯誤的處理最重要的就是「提供發生例外的相關資訊」

可以包裹第三方API,減少依賴,在未來不必花費太多力氣就可選擇更換使用另一個不同的函式庫。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
LocalPort port = new LocalPort(12)
try {
port.open();
} catch (PortDeviceFailure e) {
reportError(e);
logger.log(e.getMessage(), e);
} finally {
...
}

//LocalPort是一個簡單的包裹類別,幫助我們捕捉和翻譯從ACMEPort類別所拋出的例外事件
public class LocalPort{
private ACMEPort innerPort;
public LocalPort (int portNumber){
innerPort = new ACMEPort(portNumber);
}
public void open(){
try {
innerPort.open();
} catch (DeviceResponseExcrption e){
throw new PortDeviceFailure(e);
} catch (ATM1212UnlockedException e){
throw new PortDeviceFailure(e);
} catch (GMXError e){
throw new PortDeviceFailure(e);
}
}
...
}

Chapter 8 - 邊界

第三方軟體套件和框架的提供者,努力讓第三方軟體套件和框架具有更廣泛的適用性,使他們能夠在許多不同的環境中工作、吸引更多的使用者。從另一 方面來說使用者希望介面能夠專注於他們所要的特別需求,這種張力會導致我們的系統邊界出現問題。

我們會希望邊界上的介面被隱藏,將邊界介面放在類別裡或放在相近的類別家族裡,避免在公用API裡回傳介面,或者將介面當作參數傳給API。

學習式測試(learning test)

比照在應用程式裡的使用方式,來呼叫第三方軟體API。實質上是在做控制實驗,以檢驗我們對於這個API的了解程度。這個測試的重點在於我們想要從API獲得什麼樣的結果。(練習怎麼使用第三方的API)

Chapter 9 - 單元測試

Test-Driven Development(TDD, 測試驅動開發):先寫測試程序,然後編碼實現其功能

TDD的三大法則:

  1. 在撰寫一個單元測試(測試失敗的單元測試)前,不可撰寫任何產品程式
  2. 只撰寫剛好無法通過的單元測試,不能編譯也算無法通過
  3. 只撰寫剛好能通過當前測試失敗的產品程式

單元測試讓我們的程式保持擴充彈性、可維護性及可再利用性。

Chapter 10 - 類別

一開頭應該是一連串的變數(公用靜態變數被放在最開頭→私有靜態變數→私有實體變數)

公用函式→私有函示 (降層法則 stepdown rule)

  • 單一職責原則 (Single Responsibility Principle, SRP)
    • 類別的職責不該太多,且類別的命名應足以描述其職責
    • 一個類別或一個模組應該有一個,也只能有一個修改的理由(他負責的功能不太多)
  • 相依性反向原則(Dependency Inversion Principle, DIP):類別應該相依於抽象概念,而不是相依在具體細節上。(否則在改動細節時可能會造成整個程式碼的潰敗,也更不容易測試)
  • 凝聚性:高的凝聚性代表的是類別裡的方法和變數是互相依賴、共同形成一整個邏輯。

Chapter 11 - 系統

最常見的分離方法就是主函式 Main,將所有與建造有關的程式碼都在主函式里執行。(build放在Main function裡)

  • 相依性注入(Dependency Injection, DI):將建立的過程從使用中分離出來。最知名的DI容器就是Spring(使用@annotation),在真正需要物件前不建立物件。
  • POJO(Plain-Old Java Object) 簡單Java物件:只需要繼承Object就可以,沒有特定規定,只要建立的類別有setter/getter方法都可以稱為POJO。

Chapter 12 - 羽化

簡單設計四守則:

  1. 執行完所有的測試:為了能讓系統被測試,類別自然會變得單一、小規模,因此能符合SRP(單一職責)。且越多測試會更容易用到相依性反向的原則。→保持低耦合度與高凝聚力(Chapter10)
  2. 沒有重複的部分:在小處重複使用(為了移除重複的程式碼可能會違反SRP,因此可能又會再寫出一個更小職責的功能)
  3. 表達程式設計師的本意:選擇良好的名稱、讓函式和類別簡短、使用標準命名法
  4. 最小化類別和方法的數量

Chapter 13 - 平行化

將「做什麼」和「何時做」分開來

但是不同執行緒共享資料時容易造成結果不同(因為執行的順序路徑有太多種組合),因此建議:

  1. 跟平行化有關的程式碼要分割開來
  2. 用keyword synchronized(同步化)來限制資料視野。嚴格限制共享資料的存取次數
  3. 不去改資料,而是使用資料的複本。最後去多個執行緒蒐集這些複本
  4. 將資料劃分開來,盡量不去共享資料

Chapter 14 - 持續地精煉

程式碼需要不斷的修改,慢慢地往某一個特定目標修正,並且在過程中遵守TDD的原則,在每一次的變動中都必須確保系統能像之前一樣運作。

使用單元測試與驗收測試,以確保每一次改動並不會讓系統潰敗且能保持與此前一致的運作邏輯(或是更精美)

Other References

  • Enum的使用方法:
    • https://www.tpisoftware.com/tpu/articleDetails/1432
    • https://ithelp.ithome.com.tw/articles/10184794
  • 如何提升Java效能:https://iter01.com/186974.html