天天看點

這一團糟的代碼,真的是我寫的?!

這一團糟的代碼,真的是我寫的?!
阿裡妹導讀:你有沒有遇到過這種情況:過幾周或者幾個月之後,再看到自己寫的代碼,感覺一團糟,不禁懷疑人生?我們每天都與代碼打交道,但當被問道什麼是好的代碼時,很多人可能會先愣一下,然後給出的回答要麼比較空泛,要麼比較散,沒辦法簡單明了地概括出來。今天,我們就來說什麼是好的代碼?

一句話概括

衡量代碼品質的唯一有效标準:WTF/min —— Robert C. Martin
這一團糟的代碼,真的是我寫的?!

Bob大叔對于好代碼的了解非常有趣,對我也有很大的啟發。我們編寫的代碼,除了用于機器執行産生我們預期的效果以外,更多的時候是給人讀的,這個讀代碼的可能是後來的維護人員,更多時候是一段時間後的作者本人。

我敢打賭每個人都遇到過這樣的情況:過幾周或者幾個月之後,再看到自己寫的代碼,感覺一團糟,不禁懷疑人生。

我們自己寫的代碼,一段時間後自己看尚且如此,更别提拿給别人看了。

任何一個傻瓜都能寫出計算機可以了解的代碼。唯有寫出人類容易了解的代碼,才是優秀的程式員。 —— Martin Fowler

是以,談到好代碼,首先跳入自己腦子裡的一個詞就是:整潔。好的代碼一定是整潔的,給閱讀的人一種如沐春風,賞心悅目的感覺。

整潔的代碼如同優美的散文。 —— Grady Booch

好代碼的特性

很難給好的代碼下一個定義,相信很多人跟我一樣不會認為整潔的代碼就一定是好代碼,但好代碼一定是整潔的,整潔是好代碼的必要條件。整潔的代碼一定是高内聚低耦合的,也一定是可讀性強、易維護的。

高内聚低耦合

高内聚低耦合幾乎是每個程式員員都會挂在嘴邊的,但這個詞太過于寬泛,太過于正确,是以聰明的程式設計人員們提出了若幹面向對象設計原則來衡量代碼的優劣:

  • 開閉原則OCP (The Open-Close Principle)
  • 單一職責原則SRP (Single Responsibility Principle)
  • 依賴倒置原則DIP (Dependence Inversion Principle)
  • 最少知識原則LKP (Least Knowledge Principle)) / 迪米特法則 (Law Of Demeter)
  • 裡氏替換原則LSP (Liskov Substitution Principle)
  • 接口隔離原則ISP (Interface Segregation Principle)
  • 組合/聚合複用原則CARP (Composite/Aggregate Reuse Principle)

這些原則想必大家都很熟悉了,是我們編寫代碼時的指導方針,按照這些原則開發的代碼具有高内聚低耦合的特性。換句話說,我們可以用這些原則來衡量代碼的優劣。

但這些原則并不是死闆的教條,我們也經常會因為其他的權衡(例如可讀性、複雜度等)違背或者放棄一些原則。比如子類擁有特性的方法時,我們很可能打破裡氏替換原則。再比如,單一職責原則跟接口隔離原則有時候是沖突的,我們通常會舍棄接口隔離原則,保持單一職責。隻要打破原則的理由足夠充分,也并不見得是壞的代碼。

可讀性

代碼隻要具有了高内聚和低耦合就足夠好了嗎?并不見得,我認為代碼還必須是易讀的。好的代碼無論是風格、結構還是設計上都應該是可讀性很強的。可以從以下幾個方面考慮整潔代碼,提高可讀性。

命名

大到項目名、包名、類名,小到方法名、變量名、參數名,甚至是一個臨時變量的名稱,其命名都是很嚴肅的事,好的名字需要斟酌。

名副其實:好的名稱一定是名副其實的,不需要注釋解釋即可明白其含義的。

/**
* 建立後的天數
**/
int d;           
int daysSinceCreation;           

後者比前者的命名要好很多,閱讀者一下子就明白了變量的意思。

容易區分:我們很容易就會寫下非常相近的方法名,僅從名稱無法區分兩者到底有啥差別(eg. getAccount()與getAccountInfo()),這樣在調用時也很難抉擇要用哪個,需要去看實作的代碼才能确定。

可讀的:名稱一定是可讀的,易讀的,最好不要用自創的縮寫,或者中英文混寫。

足夠短:名稱當然不是越長越好,應該在足夠表達其含義的情況下越短越好。

格式

良好的代碼格式也是提高可讀性非常重要的一環,分為垂直格式和水準格式。

垂直格式:通常一行隻寫一個表達式或者子句。一組代碼代表一個完整的思路,不同組的代碼中間用空行間隔。

public class Demo {
@Resource
private List<Handler> handlerList;
private Map<TypeEnum, Handler> handlerMap = new ConcurrentHashMap<>();

@PostConstruct
private void init() {
if (!CollectionUtils.isEmpty(handlerList)) {
for (Handler handler : handlerList) {
                handlerMap.put(handler.getType(), handler);
            }
        }
    }

public Result<Map<String, Object>> query(Long id, TypeEnum typeEnum) {
        Handler handler = handlerMap.get(typeEnum);
if (null == handler) {
return Result.returnFailed(ErrorCode.CAN_NOT_HANDLE);
        }
return handler.query(id);
    }
}           

如果去掉了空行,可讀性大大降低:

public class Demo {
@Resource
private List<Handler> handlerList;
private Map<TypeEnum, Handler> handlerMap = new ConcurrentHashMap<>();
@PostConstruct
private void init() {
if (!CollectionUtils.isEmpty(handlerList)) {
for (Handler handler : handlerList) {
                handlerMap.put(handler.getType(), handler); } } }
public Result<Map<String, Object>> query(Long id, TypeEnum typeEnum) {
        Handler handler = handlerMap.get(typeEnum);
if (null == handler) {
return Result.returnFailed(ErrorCode.CAN_NOT_HANDLE);
        }
return handler.query(id); }
}           

類靜态變量、實體變量應定義在類的頂部。類内方法定義順序依次是:公有方法或保護方法 > 私有方法 > getter/setter方法。

水準格式:要有适當的縮進和空格。

團隊統一:通常,同一個團隊的風格盡量保持一緻。集團對于Java開發進行了非常詳細的規範。

類與函數

類和函數應短小,更短小:類和函數都不應該過長(集團要求函數長度最多不能超過80行),過長的函數可讀性一定差,往往也包含了大量重複的代碼。

函數隻做一件事(同一層次的事):同一個函數的每條執行語句應該是統一層次的抽象。例如,我們經常會寫一個函數需要給某個DTO指派,然後再調用接口,接着傳回結果。那麼這個函數應該包含三步:DTO指派,調用接口,處理結果。如果函數中還包含了DTO指派的具體操作,那麼說明此函數的執行語句并不是在同一層次的抽象。

參數越少越好:參數越多的函數,調用時越麻煩。盡量保持參數數量足夠少,最好是沒有。

注釋

别給糟糕的代碼加注釋,重構他:注釋不能美化糟糕的代碼。當企圖使用注釋前,先考慮是否可以通過調整結構,命名等操作,消除寫注釋的必要,往往這樣做之後注釋就多餘了。

好的注釋提供資訊、表達意圖、闡釋、警告:我們經常遇到這樣的情況:注釋寫的代碼執行邏輯與實際代碼的邏輯并不符合。大多數時候都是因為代碼變化了,而注釋并沒有跟進變化。是以,注釋最好提供一些代碼沒有的額外資訊,展示自己的設計意圖,而不是寫具體如何實作。

删除掉注釋的代碼:git等版本控制已經幫我們記錄了代碼的變更曆史,沒必要繼續留着過時的代碼,注釋的代碼也會對閱讀等造成幹擾。

錯誤處理

錯誤處理很重要,但他不能搞亂代碼邏輯:錯誤處理應該集中在同一層處理,并且錯誤處理的函數最好不包含其他的業務邏輯代碼,隻需要處理錯誤資訊即可。

抛出異常時提供足夠多的環境和說明,友善排查問題:異常抛出時最好将執行的類名,關鍵資料,環境資訊等均抛出,此時自定義的異常類就派上用場了,通過統一的一層處理異常,可以友善快速地定位到問題。

特例模型可消除異常控制或者null判斷:大多數的異常都是來源于NPE,有時候這個可以通過Null Object來消除掉。

盡量不要傳回null,不要傳null參數:不傳回null和不傳null也是為了盡量降低NPE的可能性。

如何判斷不是好的代碼?

讨論了好代碼的必要條件,我們再來看看好代碼的否定條件:什麼不是好的代碼。

Kent Beck使用味道來形容重構的時機,我認為當代碼有壞味道的時候,也代表了其并不是好的代碼。

代碼的壞味道

重複可能是軟體中一`切邪惡的根源。 —— Robert C.Martin

重複:Martin Fowler也認為壞味道中首當其沖的就是重複代碼。很多時候,當我們消除了重複代碼之後,發現代碼就已經比原來整潔多了。

函數過長、類過大、參數過長:過長的函數解釋能力、共享能力、選擇能力都較差,也不易維護。

過大的類代表了類做了很多事情,也常常有過多的重複代碼。

參數過長,不易了解,調用時也容易出錯。

發散式變化、霰彈式修改、依戀情結:如果一個類不是單一職責的,則不同的變化可能都需要修改這個類,說明存在發散式變化,應考慮将不同的變化分離開。

如果某個變化需要修改多個類的方法,則說明存在霰彈式修改,應考慮将這些需要修改的方法放入同一個類。

如果函數對于某個類的興趣高于了自己所處的類,說明存在依戀情結,應考慮将函數轉移到他應有的類中。

資料泥團:有時候會發現三四個相同的字段,在多個類和函數中均出現,這時候說明有必要給這一組字段建立一個類,将其封裝起來。

過多的if...else 或者使用switch:過多的if...else或者switch,都應該考慮用多态來替換掉。甚至有些人認為除個别情況外,代碼中就不應該存在if...else。

總結

本文首先一句話概括了我認為的好代碼的必要條件:整潔,接着具體分析了整潔代碼的特點,又分析了好代碼的否定條件:什麼樣的代碼不是好的代碼。僅是本人的一些見解,希望對各位以後的程式設計有些許的幫助。

我認為僅僅編寫出可運作的代碼是遠遠不夠的,還要時刻注意代碼的整潔度,留下一些漂亮的代碼。

原文釋出時間:2019-9-11

作者:馬飛翔(澤畔)

參考:

《重構——改善既有代碼的設計》Martin Fowler

《代碼整潔之道》 Robert C. Martin

《Java開發規約中文版 》阿裡巴巴集團約碼項目組

繼續閱讀