歡迎光臨
每天分享高質量文章

讀《程式碼整潔之道》

什麼是整潔程式碼

程式碼的質量非常重要,糟糕的程式碼有可能會毀了一個公司。對於一個很註重程式碼質量的人來說獃在一個只關註交付而不關註程式碼質量的公司是很痛苦的。

什麼是整潔的程式碼,不同的人又不同的定義。我認為整潔的程式碼應該是符合所使用語言程式碼規範的;可復用的;便於維護的;簡潔的。

糟糕的程式碼想做太多的事情,意圖混亂;整潔的程式碼的每個函式、類和模組都全神貫註一件事,不受周圍的幹擾,也就是設計原則中的單一原則。

命名的藝術

命名隨處可見,變數、函式、類、模組、名稱空間等都需要有好的命名。一個名稱如果能讓閱讀者一看就知道要表達的意思,就可以認為是一個好的名稱。

不要使用單個字母來做變數名,時間一長,自己都不清楚自己當初的命名是什麼意思。小方法體,如迴圈中的計數器除外。

不要使用有誤導性的字母作為變數名,比如小寫字母l和大寫字母O,因為他們和數字的一和零很像,有的字型還比較好區分,但大多數字體很難分辨。

「匈牙利命名法(HN)」:該命名法是一位叫 Charles Simonyi 的匈牙利程式員發明的,後來他在微軟獃了幾年,於是這種命名法就透過微軟的各種產品和檔案資料向世界傳播開了。在當時那個時代編譯器並不做型別檢查,程式員需要使用HN來幫助自己記住型別。現在的高階靜態程式語言具有更豐富的型別系統,編譯器可以很好的做型別檢查,所以使用NH純屬多餘。使用NH的幾個弊端:

  • 增加了名稱的長度
  • 使名稱變得不可讀
  • 增加了修改名稱的難度,修改了變數的型別,變數名就要隨著修改,否則會造成誤導。

書中講到對介面的命名不要使用“I”作為字首,這點我持保留意見,可能因為我一直是從事的.NET上的開發,.NET的類庫中的介面基本都是使用“I”作為字首的,而且在《NET 設計規範》一書中也強調介面要使用“I”作為字首。

對於方法名做到每個概念一個詞,應該保持一致,比如對於系結資料的方法,不要有的地方用BindData,而另一些地方使用DataBind,總之做到在整個程式碼中保持風格一致。

最後想說的是命名除了一些通用的法則外,對於一些規範性的問題還是要遵循所使用語言或平臺規定或是約定俗成的慣例。比如方法名C#中推薦使用Passcal風格,而在Java中則是使用Camel風格。

函式

函式一直都被要求要短小,有不少書中都以行數來作為標準,比如一個行數在20行以內被稱為小函式,或是要在5行以內才是小函式。以行數來要求似乎有些苛刻,有一些極端的情況,比如初始化一個Model,裡面有幾十個欄位,這時這個初始化函式中就有幾十行程式碼,而且是無法拆分的,所以我認為,函式只要是在做一件事情就可以了。通常來說如果函式只做一件事,自然就不會很長。我對函式長度的極限是橫豎都不要超過一屏。

函式應該只做一件事,判斷函式時候只做一件事,看函式中是否很能夠拆分,如果可以,就果斷進行重構。

當函式中有Swicth陳述句的時候,就不可避免的要做多件事情了,而且函式會隨著Switch條件的增加會越來越長。因為函式中做了多件事情,違反了SRP原則,因為隨著增加條件我們要去修改這個函式,違反了OCP原則。這個問題可以透過工廠樣式來解決。

函式的名稱要使用描述性的名稱,讓人一看名稱就知道該函式是做什麼的。當函式只做一件事情的時候,取名就容易多了。還有比較重要的一點,風格要保持一致。

在一個函式中不要去呼叫職責之外的另外的函式,尤其是底層的函式,否則給高層呼叫帶來風險。舉個簡單的例子:比如在使用者登入的時候我們可能會有一個CheckPassword的方法來驗證登入的使用者名稱和密碼,如果在CheckPassword函式中在驗證成功後呼叫Session.Init()來對Session進行初始化,就會存在隱患。根據名稱來看只是檢查密碼用,如果有人在非登入的情況下呼叫了該方法,會更改當前會話。

使用異常代替錯誤傳回碼,如果使用錯誤傳回碼會要求立即處理錯誤,當在高層呼叫很多底層方法時,每個方法都要去根據錯誤傳回碼進行處理,會造成函式邏輯混亂,如果使用異常處理則只需要在catch中處理即可。

遠離重覆,拒絕重覆,方法有很多,抽象到基類或放到底層公共類庫中。

沒有人能一次性就將函式寫的很完美,好的函式是透過重構得到的。

註釋

註釋是一把雙刃劍,好的註釋能夠給我們好的指導,不好的註釋只會將我們誤導。註釋是彌補程式碼中表述不足的一種手段,就像設計樣式是用來彌補語言不足一樣。

程式碼是我們獲取資訊的準確來源,註釋隨著專案人員的更替 反覆的修改最終可能詞不達意了,因為很多開發人員在整合程式碼,修改方法的時候常常不會修改註釋。

有時候看到一個函式的程式碼寫的很糟糕,邏輯很混亂,有開發人員可能想,給這個函式加上幾行註釋,這樣有可能起到適得其反的作用,這時要做的是將函式整理乾凈。

程式碼即註釋,很多書和大師都這麼講,意思是我們要用程式碼本身來解釋我們的意圖,那就要求我們要控制好函式只做一件事,函式名和變數名要規範和可讀。

當然也不是所有的註釋都沒有用,像下麵幾種型別的註釋是有必要的:

  • 具有警示性的註釋
  • 描述一些負責業務場景
  • 有些函式現在還是一個空殼,但在將來可能有用,有必要寫

當我們不得不寫一些註釋的時候,要確保言簡意賅,能夠很好的表達意思,不要造成誤解,也不要寫多餘的廢話。

還有一種日誌被稱為日誌式註釋,一般出現在一個類的開始部分,記錄每次修改時的時間、人員名稱和修改內容。隨著時間的推移這類註釋會變得非常冗長。這類註釋在一些專案中很普遍,而且有時會被嚴格要求寫,但書中強調現在的原始碼都會有原始碼工具來進行管理,修改記錄在原始碼工具中有儲存,這種日誌式的註釋應該全部刪除。

有的開發人員喜歡在註釋中簽上自己的名字,這種做法也沒沒有必要,因為我們有原始碼管理工具。

專案程式碼中經常會出現被註釋掉的程式碼,這對後面的維護人員會造成困擾,也會使程式碼變得混亂,這種程式碼同樣可以刪掉,因為我們有原始碼管理工具。

錯誤處理

錯誤處理簡單來說就是當軟體出現錯誤時還能正常工作。錯誤處理很重要,但不能打亂的原本的程式碼邏輯。

使用異常處理而非傳回碼,底層往上拋,最上層集中處理。這點在函式相關章節中也提到過,之所以看到有的地方是使用錯誤傳回碼,是因為早期的一些語言沒有異常處理機制,現在的語言基本都有異常處理機制。

異常的資訊應該足夠充分(包含出錯的位置以及原因)。

不要在catch塊中去實現業務邏輯,就是說當出現異常的時候一定要丟擲,而不要改變狀態或是做其他一些操作,這樣會留下很多陷進。

底層的方法不要傳回Null值,否則在呼叫時會新增很多的判斷,可以丟擲異常或傳回特例物件,特例物件是指傳回一個函式傳回值型別的空物件。

在最上層捕獲了異常後,記錄日誌,給出相應的友好結果反饋,千萬切記不要再往上拋了,例如:一個控制檯程式,如果在Main函式中捕獲到異常再往上丟擲,控制檯程式就崩潰了。

單元測試

我工作以來所經歷的公司中都很少使用單元測試,以致於我現在對單元測試這方面還不是特別熟悉,只是在自己的個人專案中寫過一些單元測試的程式碼。在.NET平臺下可以使用VS自帶的單元測試功能或是NUnit。

有一種程式設計的方法叫TDD(測試驅動開發),意思是先寫單元測試,然後寫對應的程式碼,透過修改除錯讓寫的程式碼透過單元測試。使用TDD,會使測試改寫所有的程式碼,測試程式碼和生產程式碼的比例有可能會達到1:1 ,所以也會帶來成本的問題。TDD三定律:

  • 在編寫不能透過的單元測試前,不可以編寫生產程式碼
  • 只有編寫剛好無法透過的單元測試,不能編譯也算不透過
  • 只可編寫剛好足以透過當前失敗測試的生產程式碼

測試程式碼和生產程式碼一樣的重要,也需要保持整潔。測試程式碼要隨著生產程式碼的修改而修改,否則只會產生大量無用的測試程式碼,而且也會給生產程式碼的修改帶來風險。

單元測試的好處:

  • 有了測試不用擔心對程式碼的修改
  • 有了測試可以毫無顧慮的去改進架構和設計

如果您是做專案,快速滿足客戶需求就可以了,沒有必要在專案中新增單元測試,如果是開發產品,單元測試還是非常重要的,因為產品的快速迭代就像在給高速飛行中的飛機加油一樣,不能停還要保證穩定性,不能出任何問題,而單元測試是一個很好的保障。

類通常由變數、屬性和方法組成。按照書中所講的Java的約定,類應該由一組變數開始,如果有靜態公共常量,應該放在前面,然後是私有靜態變數和私有物體變數。公共函式跟在變數之後,一些供公共函式呼叫的私有工具函式在公共函式之後。

和函式一樣,類也應該要盡可能的短小。但和函式不同不是以程式碼行數來權衡,而是以職責。如果無法準確的為某個類命名,則有可能是該類的職責過多。

單一職責原則(SRP):類或模組應該有且只有一條加以修改的理由。

在實際的工作中很多開發人員往往不會思考這麼多,他們只想著讓程式碼可以工作就可以了,所以經常出現幾千行的大類。系統應該是有許多短小的類而不是少量巨大的類組成。每個小類有單一的職責,只有一個修改的原因,所有這些小類在一起協同工作完成系統的功能。

高內聚:如果一個類中每個變數都被每個方法所使用,則該類有最大的內聚性,保持內聚性得到許多短小的類,內聚性高說明變數和方法相互關聯形成一個邏輯整體。

對於類要要良好的嗅覺,一個類在開始建立的時候,職責還是很單一的,但隨著功能越來越複雜,參與開發的人越來越多,就可能慢慢變得臃腫,需要我們能時刻保持警惕,嗅出壞味道,並做重構。

總結

很多年前看的此書,現在翻翻還是很有幫助,好書就應該時常翻閱下,不同時期會有不同的理解。

贊(0)

分享創造快樂