代碼整潔之道

  • 《Clean Code》代碼整潔之道 一書相關讀書筆記,整潔的代碼是自解釋的,閱讀代碼應該如同閱讀一篇優秀的文章,見字知意,可以一會兒明白大概的代碼功能。代碼首先要能讀懂,其次纔去要求功能實現。java

  • 做爲開發者來講,在如今基本都講究團隊合做的工做方式下,規範遠比功能重要,開發的功能也許在版本迭代中就不復存在了,可是規範倒是一直存在的。正則表達式

  • 爲了整潔的代碼,就必須遵循一些統一的團隊規範。混亂不堪連命名都沒法取好的代碼,只會給增長後續接手人員維護的成本數據庫

第一章 整潔代碼

1.1-1.9

本章主要表述糟糕的代碼對項目進度的提高並沒有太多的幫助,只會如同一堆朝不保夕的柴火同樣,後續開發者又會理不清關係常常直接接着往上面扔其它柴火,最終結果就是轟然倒塌沒法維護。因此在一開始咱們便要最大程度的保持代碼整潔。編程

所謂的整潔代碼,我的理解大概是符合如下要求: 1.代碼格式統一一種規範,好比縮進、空行、空格等等; 2.使用有意義的命名,包含類名、變量名、函數名等等,可以一眼就基本看出所要完成的大概操做; 3.類、函數功能單一簡短,避免臃腫以及過多嵌套;設計模式

第二章 有意義的命名

2.1-2.6

可以讀出單詞意思,符合實際用處的變量命名讓代碼閱讀過程更加愉悅,再簡潔的代碼一旦命名糟糕都會致使模糊度增長,難以讀懂意思。api

2.7-2.18

類名、對象名一般都是名詞/名詞短語,方法名通常都是動詞/動詞短語。命名時結合上下文的關聯性進行命令,能夠產生有意義的語境。例如score和student、exam等詞語在一塊兒時,容易讓人聯想到score表明學生考試的成績,固然也有能夠添加必要的前綴增長語境,例如examScore數組

第三章 函數

3.1-3.3

函數的第一規則是短小,第二規則是更短小,函數長度最好不超過一屏幕,不然須要來回滾動,閱讀不方便;安全

每一個函數功能單一,嵌套不超過2層,只作一件事,作好這件事。判斷是否功能單一,能夠看是否還能夠再抽出一個函數;bash

越是短小的函數,功能越是單一,命名就更加針對性;服務器

自頂向下的閱讀規則,每一個函數一個層級並調用下一個層級函數;

3.4-3.5

switch語句的最好在較低的抽象層級上使用,用於建立多態。 使用描述性的名稱,別懼怕名稱太長。長而具備描述性的名稱要比短而使人費解的名稱好。

3.6-3.7

最理想的參數數量是零,沒有足夠的理由不要使用3個及以上的參數數量,不只增長了理解成本,並且從測試角度看,參數組合對編寫單元測試來說即是麻煩事。當參數數量太多,應該考慮整合部分參數爲類對象。

部分函數所執行的處理可能隱藏着與函數名不相符的操做,這種函數具備破壞性,違反了單一原則。好比在void checkPassword(String username, String password)函數中若是隱藏着Session.Initialize()會話初始化操做,可能會致使引用的人只是想判斷用戶名和密碼是否正確,結果卻致使了當前正常的會話出現了問題,正確的作法能夠是該函數命名爲checkPasswordAndSessionInitialize(String username, String password)

上述這2點也是平時編程中容易犯的錯誤: 參數數量的過多,一方面增長了閱讀理解函數名稱的時間成本,在閱讀具體實現中,還要時刻注意各個參數的具體引用位置與效果; 在一個api中處理過多的事情,而名稱有沒有描述清楚,會致使不清楚具體實現的人進行調用後而出現的未知的錯誤;

3.8-3.15

消除重複代碼,重複是軟件中的罪惡之源,許多原則與實踐都是爲了消除重複而建立。去除過多的冗餘,讓代碼邏輯一目瞭然,也讓後續修改維護更加簡單。

像寫一篇文章對待代碼,初稿也許粗陋無序,你就斟酌推敲,直到達到你心目中的樣子。

第四章 註釋

4.1 - 4.3

寫註釋的動機之一是爲了由於糟糕代碼的存在,怕別人看不懂而決定添加點註釋,但最好的處理是把代碼弄乾淨,而不是去添加那些沒有必要的註釋,要相信好的代碼是"自說明"的;

註釋都有可能存在誤導、不適用或者提供錯誤的信息,也許源於一開始就表述錯誤的註釋,也許由於後續代碼已經修改但卻未及時更新註釋等等緣由致使;

對於引用某些沒法修改的第三方庫文件返回的晦澀難懂的返回值,適當給予註釋說明,闡釋含義有助於理解;

對於相對複雜的處理流程,註釋解釋做者的意圖能夠提供理解相關實現的有用信息;

編寫公共API就該爲它編寫良好的標準的java doc說明。

4.4 - 4.5

壞的註釋存在如下狀況: 1.多餘的註釋:簡單明瞭的函數,其函數註釋說明純屬多餘,讀註釋的時間可能多餘讀函數代碼的時間;

2.誤導性的註釋:註釋不夠精確,存在與實際運行不相符的狀況,誤導使用者進行錯誤的處理;

3.日誌性的註釋:在每一個模塊開始添加每一次修改的註釋性日誌已經沒有必要,如今不少代碼維護工具提供更好的修改記錄;

4.廢話性的註釋:好比private int dayofMonth ; 註釋爲 the day of the month ;

5.循規性的註釋: 每一個函數或者每一個變量都有javadoc標準的註釋徹底沒有必要,除非是編寫公共API;

能用函數和變量自說明的時候,就別添加任何註釋。註釋自己是爲了解釋未能自行解釋的代碼,若是註釋自己還須要解釋,那就毫無做用。

第五章 格式

5.1

代碼格式很重要,由於代碼的可讀性會對之後可能發生的修改行爲產生深遠的影響。我也曾認爲"讓代碼能工做"纔是頭等大事,後面慢慢的會發現,所編寫的功能在下一個版本也許就被修改掉不復存在了,但代碼格式規範確實一直存在下去的。良好的代碼格式能夠促進理解溝通,在協同合做的當下,注意規範很重要。

5.2 - 5.5

每一個人可能偏好的代碼格式不一樣,可是團隊合做中就該商量統一一種好的代碼格式,全部人都按照這種標準進行開發工做。對於代碼的垂直格式,好比類文件儘可能功能簡單,注意能拆分的進行拆分,以避免類過大會變得臃腫難以維護。在功能描述上注重從名稱上就能夠大概瞭解主要處理的功能點。對於代碼的水平格式,一行代碼儘可能不超過一個屏幕的顯示範圍。

第六章 對象與數據結構

6.1 - 6.5

這章主要講的是對象與數據結構優缺點。對象主要提供接口而隱藏其內部實現,而數據結構提供純數據讀寫而不帶任何有其它意義的函數操做。

在個人理解上: 過程式編程主要是單獨使用業務類去處理各類數據結構,這就致使了在各個方法中傳參爲Object對象而後須要用判斷語句判斷每一種數據結構對應的類型,纔好方便作到根據傳入的不一樣數據結構類型來返回不一樣的結果。這種方式好處就是業務類中增長了新的操做函數,並不須要去修改已存在的數據結構類,可是若是增長了新的數據結構類型則須要在業務類中的每一個操做函數中增長該結構類型的判斷;

面向對象由於推崇接口式編程,若是一開始接口沒考慮清楚,那麼增長減小接口就會致使全部實現類都須要進行修改,可是相比較下面向對象方便在建立新的類對象時,不影響其它對象的操做接口,由於實現上是多態的。 前者方便修改已存在的操做函數但難以增長類,後者方便增長類,但難以修改已存在的接口。並無孰優孰劣,須要根據實際去決定採用哪一種。

第七章 錯誤處理

7.1-7.2

這2個小節主要講了錯誤處理中的2個建議,第1個是使用異常代替返回碼,第2個是對於要捕獲的異常先書寫Try-Catch-Finally語句。 對於第1個書上說是爲了代碼書寫過程當中不須要每次調用都要去判斷返回的各類錯誤碼的狀況,影響代碼整潔會打亂代碼邏輯。但有時候實際使用過程,仍是常常會有將錯誤碼定義爲返回值的狀況,對調用者來說方便判斷結果,通常這種狀況的錯誤碼都會有不少種類型,甚至存在不一樣的錯誤代碼可能都當作成功處理的狀況,只是單單使用拋出異常的方式有時候並非太好。 對於第2個,這是一種好的代碼規範,對於須要捕獲錯誤的函數,理論上try開頭,finally結尾。

7.3-7.10

若是底層api已經被普遍使用,須要拋出新異常,建議內部封裝處理好,避免上層每個環節都須要從新throw或catch該新的異常。根據實際使用的狀況,對於主動拋出的自定義異常最好進行分類,而且攜帶異常說明,方便排查是什麼緣由致使。函數的處理上,別最好避免傳入null和返回null的狀況,避免引用的地方都要增長非null判斷,影響整潔。

第八章 邊界

8.1-8.7

使用第三方代碼或者開源庫的時候,避免直接在生產代碼中驗證功能,能夠嘗試編寫對應的單元測試來熟悉其使用方法,理解提供的api接口,這樣當開源庫版本發生變化,能夠快速根據以前編寫的單元測試來驗證功能是否會受到影響。對於開源庫/第三方提供的接口,能夠嘗試再進行一層封裝,使用方便本身理解的接口名,避免代碼各處直接引用其提供的接口,這樣之後即使替換爲不一樣的開源庫或者開源庫接口發生變動,也能夠作到改動最小。

第九章 單元測試

9.1-9.3

這幾個小節主要講述單元測試的重要性,單元測試與生產代碼並行甚至優先於生產代碼誕生,這樣保證項目完成之時有着覆蓋各個方面的單元測試體系。不能由於是單元測試就雙重標準判斷,不去注重單元測試的整潔。一套好的單元測試可讓你無所顧忌的修改現有代碼的框架,也是你進行大改動的底氣,而同生產代碼同樣,整潔的單元測試是必不可少的,不然一旦成套的單元測試臃腫不堪難以維護,會致使單元測試沒法跟上生產代碼的更新速度,最終變爲效率上的累贅,一旦拋棄又沒法保證主體功能的穩定。

實際開發中,其實每每忽略了單元測試的編寫,我感受這不只僅是由於項目週期趕的問題,而是咱們一開始就沒有將單元測試的週期安排進去。因爲這習慣性的忽略,每每是每次修改,反而須要耗費更多時間去自測。

9.4-9.7

單元測試用應該要儘量少的斷言操做,而且保證測試用例只覆蓋一個點。整潔的測試用例符合"First"原則: 快速(Fast),測試速度快;

獨立(Independent),用例之間相互獨立,互不影響和依賴;

可重複(Repeatable),測試可在任務環境下重複使用,好比有網絡、無網絡,生產環境或者質檢環境;

自足驗證(Self-Validating),用例不該該須要人工額外操做來判斷是否經過,好比查看日誌文件等等。用例自己應該直接經過斷言判斷是否成功。

及時(Timely),測試用例應該及時編寫,而且最好優先於生產代碼編寫。不然先編寫生產代碼的話,每每會發現生產代碼難以測試。
複製代碼

第十章 類

10.1 - 10.2.1

類的規則與函數相似,第一規則是短小,第二規則仍是短小。越是短小的類越能符合單一職責原則。 項目工程達到必定程度後,類一不當心就會臃腫,須要再將類進行分割成一個個單一職責的小類,整潔的系統就該由許多短小的類而不是少許巨大的類來組成。

10.2.2-10.4

類的設計應該體現內聚性,即模塊內的各個元素應該緊密的聯繫在一塊兒。這邊包含類中成員變量與方法命名上的要有關聯性,類中成員變量應該儘可能被更多的類方法所操做,這樣會使得方法與變量更加粘聚。內聚性高,意味着方法與變量相互依賴,互相結合成一個單一總體,職責也就越單一。 若是類中某些變量只被少數幾個方法共享,意味着這幾個方法和變量能夠再單獨拆分出一個小類出來,不然堆積了愈來愈多的這種變量將會致使內聚性的下降。

第十一章 系統

11.1 - 11.2

這幾個小節主要講「將構造與使用相互分離"的好處,書本描述得有點難以理解,意思大概就是: 構造與使用分離,會使得類之間的耦合度降低,方便後續進行實現上的替換修改。 舉個相對簡單的例子說明:

public class HandlerImpl {
                public void handler() {
                    Component component= new component();
                    component.handle();
                }
            }
複製代碼

在這個例子中,因爲構造與使用沒有分離,即HandlerImpl便是Component的構造者(調用了構造函數),又是Component的執行者(調用了方法),在以後假設須要將Component的實現替換爲接口實現類,或者其它類,就會致使整個系統中不少相似這樣的地方都須要進行替換爲 Component component= new ComponentImpl()、Component2 component= new Component2();這樣修改太多,耦合度過高,影響後續維護。

相似"分離構造與使用"可使用相似工廠模式+面向接口編程,如:

public class HandlerImpl {
                public void handler() {
                    Component component= ComponentFactory.getComponent();
                    component.handle();
                }
            }
            
            public class ComponentFactory {
                public static Component getComponent() {
                    Component component= new componentImpl1();
                    return;
                }
            }
複製代碼

後續即使替換Component的實現,或者增長對使用Component條件的判斷,都只須要修改構造器ComponentFactory而不須要取修改全部使用的地方:

public class ComponentFactory {
                public static Component getComponent() {
                    if (...) {
                       return new componentImpl1(); 
                    }
                    if (...) {
                        return new componentImpl2(); 
                    }
                }
            }
複製代碼

11.3 - 11.12

這幾個小節主要講述了系統整潔,要達到系統整潔,須要注意模塊功能的劃分,注意簡單的Been對象不與業務邏輯耦合在一塊兒,作到職責單一。項目初始階段,就將將來系統方方面面考慮周全,提供一大堆目前尚不要求的API或者功能點是沒必要要的,所要作到的是劃分好各個功能模塊,注意各個模塊之間的可擴展性與相互隔離。這樣方便後面不斷進行擴展和重構優化,快速迭代敏捷開發。

橫貫式關注面? 持久化? AOP ?EJB?表示這章有點懵,看不太懂?
複製代碼

第12章 迭進

如何達到簡單整潔的設計,其實有四條規則能夠遵照,按重要性從大到小排序爲: 1.運行全部的測試: 這意味着須要編寫單元測試,也是不少人所欠缺的,完整的單元測試會迫使你在編碼過程注重可測性,也就必須劃分好模塊功能,遵照好單一職責。完善的測試也是不斷重構的底氣。

2.消除重複: 重複是良好設計的天敵,各類優秀的設計模式也是爲了消除重複。不光是雷同的代碼段重複,還有其它行爲的重複,好比功能上能夠支持同樣的效果(像是int getSize()和boolean isEmpty(),這時候其實isEmpty()沒什麼必要),過多的重複會有沒必要要的風險和增長額外的複雜度。

3.表達清楚意圖: 這就要求命名要準確明瞭,上下文關聯性強,可以閱讀出做者的意圖。

4.儘量減小類和方法的數量: 各類設計模式和功能上的劃分每每會伴隨類和方法數量的增長,但並非說類和方法越多越好。有些能夠避免的就該避免掉,像是爲每一個類都建立接口類其實並無必要。

第13章 併發

13.1 -13.3

併發編髮能夠對"時機"和"目的"解耦,不像單線程將2者緊密耦合在一塊兒。解耦2者的目的,能夠明顯改進應用程序的吞吐量和結構。結構上看更像是多臺計算機同時工做,而不是一個大循環,系統所以更容易被理解,可以更好地切分關注面。併發編程的是一把雙刃劍,大部分狀況下能夠顯著提升性能,可是也有須要注意的地方: 1.數據共享後存在多線程訪問修改的同步問題; 2.根據狀況也可以使用數據複本代替同步數據塊來避免共享數據,後續統一彙總複本合併修改; 3.併發會在性能和編寫額外代碼上增長一些開銷; 4.每一個線程儘量只運行在本身的小世界,儘可能不與其它線程共享數據

13.4 - 13.11

講述了併發編程可能會致使死鎖、互斥、等待等等狀態,要編寫好的併發代碼,須要遵循單一職責原則。將系統切分爲線程相關代碼和與線程無關的代碼的POJO,確保代碼簡單,目的集中。須要瞭解和學習相關併發庫的知識,學會分析併發問題產生的緣由。注意鎖定的代碼區域儘量小,避免下降性能。測試多線程須要注意在不一樣平臺下、數量不等的線程以及使用各類wait、sleep、yield、priority裝置代碼,改變代碼的運行順序,用以儘早的發現各類問題。

第14章 逐步改進

前面章節有講述過好的代碼就像好的散文同樣,可以讓人愉快的閱讀。而散文每每須要經歷過各類草稿階段方能成正稿,代碼同樣是這個道理,須要持續優化修改,方能符合整潔之道。 這章強調的是逐步改進,每改進一步,就完整運行伴隨生產代碼產生的各類測試用例,確保每一步修改都不會留下問題,不會影響下一步修改,循循漸進,最終完成代碼總體的重構。也再一次說明,完善的測試用例很重要。

第15章 JUnit內幕

這章以幾個單元測試用例爲例,講解用例從凌亂臃腫到整潔合理的修改過程。主要是對前面幾章講解的代碼整潔之道的應用,方法單一職責、命名準確優雅,從方法、變量以及排版上重構出整潔的代碼

第16章 重構SerialDate

依舊舉個例子講解修改過程: 完善測試用例,逐步修改逐步測試; 去除一些多餘的註釋,對於星期使用枚舉代替int類型避免須要進行有效值判斷; 使用工廠模式完成實體建立,實現構造與使用的分離; 優化方法和變量的命名去除重複的代碼、消除魔術數的存在;

第17章 味道與啓發

章節17.1 -17.4

講述總結了須要優化的地方:

對於註釋: 1.不恰當的信息,好比修改歷史; 2.廢棄的註釋,即過期、無關或者不正確的註釋; 3.冗餘的註釋,例如除了函數簽名什麼都沒有註明的javadoc; 4.註釋掉的代碼塊,不明其意的糟糕註釋;

對於函數: 1.參數越少越好,最多不超過3個; 2.避免輸出參數,應使用返回值或者參數對象狀態代替; 3.避免標識參數,例如布爾值參數; 4.去除沒有調用的方法;

通常性問題: 1.去除重複代碼塊; 2.保證邊界代碼正確執行; 3.正確的抽象層級; 4.多態代替多重判斷操做; 5.命名清楚表達意圖; 等等

章節17.5 -17.9

java中: 1.能夠說使用通配符來避免過長的導入清單,好比import package.*; 2.避免從最低繼承體系中繼承常量; 3.使用合適的枚舉代替常量;

名稱: 1.採用準確的描述性名稱,統一規範; 2.避免有歧義的名稱,不畏懼使用較長的名稱;

測試: 1.測試用例先行,早於生產代碼一步; 2.邊界測試完善; 3.不忽略小細節的測試; 4.測試速度要足夠快速; 5.對於失敗的測試用例要更加全面的進行測試;

附錄 A 併發編程

A.1 - A.2

高併發網絡通訊性能測試中如何有效提升服務器吞吐量,主要取決於服務器處理的處理方法是與I/O有關的話就能夠經過建立儘量多的線程來達到性能的提高,固然也要作個最大量的限制避免超過JVM容許的範圍。若是是由於CPU數量限制的話,那隻能經過提高硬件來解決了。2者常見的處理範圍: I/O---使用套接字、連接到數據庫、等待虛擬內存交換等; 處理器---數值計算、正則表達式處理、垃圾回收等; 另外編寫服務器代碼時,須要注意對各個功能職責的劃分,避免堆砌在一塊兒。

高併發下還須要注意代碼執行路徑的問題,未加鎖的代碼在多線程訪問的狀況下,每每獲取到的結果前期百怪,很大程度上是普通的java語句生成字節碼後每每是一句java對應多句字節碼,併發編程下,線程訪問就有可能處在各個字節碼步驟下。

A.3瞭解類庫 - A.4方法之間的依賴可能破壞併發代碼

1.java自己就提供了不少併發的類庫處理,像是Executor線程池框架,咱們能夠直接使用方便支持一些簡單的併發操做; 2.Synchronized鎖定操做以及CAS能夠很大程度上保證併發下資源訪問的線程安全,大部分狀況下CAS的效率都會比Synchronized高; 3.有些類操做並非線程安全的,好比數據庫鏈接、部分容器類操做等; 4.對於多個單方法線程安全的操做,組合起來就不必定是安全的,方法之間的依賴可能破壞併發代碼,好比如下2個例子:

案例一:

對於HashTable map,存在如下2個線程安全的操做:
public synchronize boolean containsKey(key);
public synchronize void put(key, value);

可是組合起來要實現假設map不存在才容許添加的操做時:
if(!map.containsKey(key)) {
        map.put(key, valie);
    }
複製代碼

單線程執行下並不會有啥問題,可是高併發下就有機率出現線程1經過containsKey的判斷後,可能cpu時間片切換等問題,線程1等待,輪到線程2執行也經過了containsKey,這時候就會出現線程1和線程2都會排隊執行到put操做,與本來期待不符合。

解決辦法:

能夠是整合成一個api操做進行鎖定:
public synchronize void putIfAbsent(key, value) {
    if(!map.containsKey(key)) {
        map.put(key, valie);
    }
}
或者受到調用前手動鎖定代碼塊:
synchronize(map) {
    if(!map.containsKey(key)) {
        map.put(key, valie);
    }
}
複製代碼

案例二:

public class IntegerIterator {
    Integer value = 0;
    public synchronize boolean hasNext(){
        if (value < 100){
            return true;
        }
        return false;
    }
    
    public Integer Integer next() {
        if(value == 100) {
            throw new xxxException();
        }
        return value++;
    }
}
複製代碼

使用:

IntegerIterator iterator = new IntegerIterator();
while(iterator.hasNext()) {
    int value = iterator.next();
    // ...
}
複製代碼

該例子一樣在高併發下容易出問題,可是可能須要好久才能出現而且難以跟蹤,由於須要再邊界時value = 99,此時多個線程同時經過了hasNext判斷後,後續就會拋出對應的異常。解決方法相似案例一

A.5 - A.10

多線程下對有限資源的爭奪容易形成死鎖、互斥、循環等待等等異常狀況。在實際使用中咱們能夠優化代碼減小此類狀況的發生,好比知足線程資源的搶先機制,規劃好各線程資源獲取的順序等等。因爲併發引發的問題難以發現與模擬,能夠嘗試借用一些工具進行測試,好比IBM ConTest(表示沒下載到)

微信公衆號

相關文章
相關標籤/搜索