前言
在開發代碼的時候,經常覺得代碼有一種壞味道,有一種迫切重構的想法,但是再一次的仔細閱讀代碼,卻發現為了實作業務需求,代碼就應該是現有這種模式的寫法。之前也有在網上看一些代碼重構的文章,但是直到看完《代碼整潔之路》這本書,才算是有了一些明确代碼重構的方法和思路。在這次對生産代碼的一些重構實踐中,對文章中提到并親身體會過的部分小點,有了一些認識和體會,記錄如下。
參考部落格中的第一篇,重構十六字心法是下述小點的前提:舊的不變,新的建立,一步切換,舊的再見;
過長的函數及過長的函數參數清單
過長的函數參數清單導緻的函數一定是會過長的,因為會存在參數的校驗和參數的進一步加工處理,這些是可以将參數進行封裝到資料結構以後提前進行處理的,同時也能大大減少函數調用長度,讓閱讀者更加清楚的聚焦函數内部邏輯處理。
過長的函數處理體是很難讓人一下理清處理邏輯,它需要開發者一行一行的閱讀完整個函數才能有大概的了解,它代表着函數内部的代碼抽象層級不統一,這個時候需要我們對其進行分治,将一些聚合在一起的邏輯封裝為一個函數,這樣子我們一個大函數裡面隻有幾個子函數調用,通過子函數名我們就能知道這個函數的大概處理邏輯。
過長的判斷條件
當我們在if 判斷條件中,使用 || &&連接配接的判斷語句超過兩個,我覺得就可以被認定為過長的判斷條件。這種寫法在代碼閱讀的時候,要求閱讀者能夠記住判斷條件中使用的變量代表中的意義,有一定的記憶成本。另外這種過長的判斷條件,還是有很大一部分的重複使用的情況,此時将它封裝為一個單獨函數,并起名一個有意義的名字,友善後續的閱讀和維護。
變量的有效命名
變量的有效命名和函數的有效命名是一樣的,一個可以望文生義的名字可以節省維護代碼很大一部分時間,書中對此點進行了反複的強調。一個有意義的名字不是立即取出來的,當我們發現有更适合的命名時,一定要毫不猶豫的更改掉。
函數中的輸出參數
類似于如下代碼,函數在處理的時候,直接從params擷取參數處理後并将結果put到Map中去。
void process(Map<String, Object> params);
在我們的生産代碼中有很多這種的函數處理,當我去閱讀的時候,給我帶來很大的認知困擾。這種函數會某個代碼片段中被調用一下,并沒有傳回結果,這個行為是非常容易迷惑人的,因為參數被自然的看做是函數的輸入。如果該函數沒有傳回結果,對于這種的代碼,我認為是可以随意調整的,但是随後又會存在一個函數依賴此函數的處理結果,而這些函數的命名或者參數中并沒有展現出依賴關系,很容易出現嚴重的BUG。
我們應該極力避免輸出參數的使用,即使是簡單的将輸入參數作為結果傳回也比傳回Void好很多。
函數名和函數實際行為的比對,避免副作用
正如變量的有效命名所說,函數名必須有意義,但是函數行為也必須和函數名比對,不能導緻其它的副作用。check、is等這些開頭的函數不應該對對象的狀态進行更改。
垃圾的代碼上增加注釋
很多時候,我會在晦澀難懂的代碼上增加注釋以增強自己的了解,但往往下一次閱讀的時候還是沒有辦法一目了然,仍然需要一行行從頭閱讀才能大概了解其意思。更加困難的一個事情,如果這個代碼實作具備一定的業務背景,在交接的時候并沒有提及的話,後來者很難閱讀出其意圖,更加别提去維護了。
如果代碼實作的不好,不要增加注釋了,重構它吧。
循規蹈矩的JavaDoc注釋
如果是僅僅為了避免ide的提示,給函數增加上了循規蹈矩的Java doc注釋,那還是幹掉它吧。多餘的注釋隻會分散開發者的注意力,另外不正确注釋比不好的代碼實作更加容易讓人誤解。沒有注釋的情況下,開發者使用的時候會檢視實作,但是有注釋的情況下,很可能會以注釋内容為準,進而導緻錯誤的行為。
統一變量命名概念
辨別同一個概念意義的變量,命名一定要盡量統一。比如說分頁的頁碼辨別,pageNo、currentPage、pageNumber等混用情況。如果是其它更加複雜帶有業務屬性的變量,同一個概念使用不同的變量辨別會帶來更加嚴重的認知困難。
盡量避免傳回null值
Java中的NullPointException是我們在程式設計希望極力避免掉的,是以我們在調用某個可能傳回null的函數時,我們都需要增加判斷條件,當傳回值不為null才進行進一步的處理,所有函數調用初都不可避免的帶上了這個判斷條件處理,影響代碼的閱讀。
從另外一個角度來說,當函數提供者實作時,發現有調用方擷取一個不存在的值,傳回null也是一種可以了解的行為,在我的實作中就經常出現該類代碼。之前我一直沒有思考過不傳回null,直接抛異常的處理方式,但這次去回看某些函數實作,正确的行為就應該是當結果為null時,抛出業務異常。
這一重構點我目前覺得大部分場景都難以避免,但是在後續實作業務邏輯的時候,遇上傳回null的場景,一定要思考下抛出異常的處理邏輯,這種實作能夠確定函數調用方一定能夠拿到有值的結果,進而避免的重複、無味的非空判斷。
火車頭事件,代碼的封裝
當A依賴B,B依賴C時,如果A實作的一個邏輯處理,實際依賴于C的處理,我們有時會讓A使用B拿到C以後,然後A直接調用C進行處理,這種行為被作者稱為火車頭事件。A直接調用C的行為,打破了B的封裝,讓A直接感受到了B的内部邏輯。
正确的行為應該是B封裝C的行為,A隻會感覺到B,A調用B後,由B再來調用C。這個場景在我的代碼中經常出現,當A、B、C都是自己編寫的情況下,這個打破封裝的行為自己是完全意識不到的,需要我們回過頭來審視自己的代碼,重新思考代碼的正确行為。
重構時的單測
程式員都對自己的代碼非常自信,特别是大部分的業務邏輯都是簡單的增删改查的情況下,我自己單測是寫得比較少的。但是錯誤往往出現于細微之處,特别在大部分正确的情況下,細小處的錯誤行為是很難尋找的。書中作者在講述重構樣例時,花了很大一番功夫來描述單測。如何讓單測能夠覆寫函數的所有行為是對程式員的一個考驗,寫好單測是重構的先決條件,不然往往會讓自己事倍功半。
一定要有單測,TDD!
避免重複
Do not repeat youself! 這個原則被大部分的重構指引提及,而現代的ide能夠智能識别到重複代碼,另外當你發現你自己寫了似曾相似的處理代碼,那麼你也在重複自己了,這種時候就應該提醒自己回頭審視自己的代碼了,一定是我們的處理邏輯存在問題才會導緻這種的重複。當發現重複時,多回頭看看!