阿里工程師談什么是好的代碼?
一句話概括
衡量代碼質(zhì)量的唯一有效標(biāo)準(zhǔn):WTF/min —— Robert C. Martin
Bob 大叔對于好代碼的理解非常有趣,對我也有很大的啟發(fā)。我們編寫的代碼,除了用于機(jī)器執(zhí)行產(chǎn)生我們預(yù)期的效果以外,更多的時(shí)候是給人讀的,這個(gè)讀代碼的可能是后來的維護(hù)人員,更多時(shí)候是一段時(shí)間后的作者本人。
我敢打賭每個(gè)人都遇到過這樣的情況:過幾周或者幾個(gè)月之后,再看到自己寫的代碼,感覺一團(tuán)糟,不禁懷疑人生。
我們自己寫的代碼,一段時(shí)間后自己看尚且如此,更別提拿給別人看了。
- 任何一個(gè)傻瓜都能寫出計(jì)算機(jī)可以理解的代碼。唯有寫出人類容易理解的代碼,才是優(yōu)秀的程序員。—— Martin Fowler
所以,談到好代碼,首先跳入自己腦子里的一個(gè)詞就是:整潔。
好的代碼一定是整潔的,給閱讀的人一種如沐春風(fēng),賞心悅目的感覺。
- 整潔的代碼如同優(yōu)美的散文。—— Grady Booch
好代碼的特性
很難給好的代碼下一個(gè)定義,相信很多人跟我一樣不會認(rèn)為整潔的代碼就一定是好代碼,但好代碼一定是整潔的,整潔是好代碼的必要條件。整潔的代碼一定是高內(nèi)聚低耦合的,也一定是可讀性強(qiáng)、易維護(hù)的。
高內(nèi)聚低耦合
高內(nèi)聚低耦合幾乎是每個(gè)程序員員都會掛在嘴邊的,但這個(gè)詞太過于寬泛,太過于正確,所以聰明的編程人員們提出了若干面向?qū)ο笤O(shè)計(jì)原則來衡量代碼的優(yōu)劣:
- 開閉原則 OCP (The Open-Close Principle)
- 單一職責(zé)原則 SRP (Single Responsibility Principle)
- 依賴倒置原則 DIP (Dependence Inversion Principle)
- 最少知識原則 LKP (Least Knowledge Principle)) / 迪米特法則 (Law Of Demeter)
- 里氏替換原則 LSP (Liskov Substitution Principle)
- 接口隔離原則 ISP (Interface Segregation Principle)
- 組合 / 聚合復(fù)用原則 CARP (Composite/Aggregate Reuse Principle)
這些原則想必大家都很熟悉了,是我們編寫代碼時(shí)的指導(dǎo)方針,按照這些原則開發(fā)的代碼具有高內(nèi)聚低耦合的特性。換句話說,我們可以用這些原則來衡量代碼的優(yōu)劣。
但這些原則并不是死板的教條,我們也經(jīng)常會因?yàn)槠渌臋?quán)衡(例如可讀性、復(fù)雜度等)違背或者放棄一些原則。比如子類擁有特性的方法時(shí),我們很可能打破里氏替換原則。再比如,單一職責(zé)原則跟接口隔離原則有時(shí)候是沖突的,我們通常會舍棄接口隔離原則,保持單一職責(zé)。只要打破原則的理由足夠充分,也并不見得是壞的代碼。
可讀性
代碼只要具有了高內(nèi)聚和低耦合就足夠好了嗎?并不見得,我認(rèn)為代碼還必須是易讀的。好的代碼無論是風(fēng)格、結(jié)構(gòu)還是設(shè)計(jì)上都應(yīng)該是可讀性很強(qiáng)的。可以從以下幾個(gè)方面考慮整潔代碼,提高可讀性。
命名
大到項(xiàng)目名、包名、類名,小到方法名、變量名、參數(shù)名,甚至是一個(gè)臨時(shí)變量的名稱,其命名都是很嚴(yán)肅的事,好的名字需要斟酌。
名副其實(shí)
好的名稱一定是名副其實(shí)的,不需要注釋解釋即可明白其含義的。
- /**
- * 創(chuàng)建后的天數(shù)
- **/
- int d;
- int daysSinceCreation;
后者比前者的命名要好很多,閱讀者一下子就明白了變量的意思。
容易區(qū)分
我們很容易就會寫下非常相近的方法名,僅從名稱無法區(qū)分兩者到底有啥區(qū)別(eg. getAccount()與getAccountInfo()),這樣在調(diào)用時(shí)也很難抉擇要用哪個(gè),需要去看實(shí)現(xiàn)的代碼才能確定。
可讀的
名稱一定是可讀的,易讀的,最好不要用自創(chuàng)的縮寫,或者中英文混寫。
足夠短
名稱當(dāng)然不是越長越好,應(yīng)該在足夠表達(dá)其含義的情況下越短越好。
格式
良好的代碼格式也是提高可讀性非常重要的一環(huán),分為垂直格式和水平格式。
垂直格式
通常一行只寫一個(gè)表達(dá)式或者子句。一組代碼代表一個(gè)完整的思路,不同組的代碼中間用空行間隔。
- 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);
- }
- }
- }
- publicResult<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); }
- }
類靜態(tài)變量、實(shí)體變量應(yīng)定義在類的頂部。類內(nèi)方法定義順序依次是:公有方法或保護(hù)方法 > 私有方法 > getter/setter 方法。
水平格式
要有適當(dāng)?shù)目s進(jìn)和空格。
團(tuán)隊(duì)統(tǒng)一
通常,同一個(gè)團(tuán)隊(duì)的風(fēng)格盡量保持一致。集團(tuán)對于 Java 開發(fā)進(jìn)行了非常詳細(xì)的規(guī)范。(可點(diǎn)擊下方閱讀原文,了解更多內(nèi)容)
類與函數(shù)
類和函數(shù)應(yīng)短小,更短小
類和函數(shù)都不應(yīng)該過長(集團(tuán)要求函數(shù)長度最多不能超過 80 行),過長的函數(shù)可讀性一定差,往往也包含了大量重復(fù)的代碼。
函數(shù)只做一件事(同一層次的事)
同一個(gè)函數(shù)的每條執(zhí)行語句應(yīng)該是統(tǒng)一層次的抽象。例如,我們經(jīng)常會寫一個(gè)函數(shù)需要給某個(gè) DTO 賦值,然后再調(diào)用接口,接著返回結(jié)果。那么這個(gè)函數(shù)應(yīng)該包含三步:DTO 賦值,調(diào)用接口,處理結(jié)果。如果函數(shù)中還包含了 DTO 賦值的具體操作,那么說明此函數(shù)的執(zhí)行語句并不是在同一層次的抽象。
參數(shù)越少越好
參數(shù)越多的函數(shù),調(diào)用時(shí)越麻煩。盡量保持參數(shù)數(shù)量足夠少,最好是沒有。
注釋
別給糟糕的代碼加注釋,重構(gòu)他
注釋不能美化糟糕的代碼。當(dāng)企圖使用注釋前,先考慮是否可以通過調(diào)整結(jié)構(gòu),命名等操作,消除寫注釋的必要,往往這樣做之后注釋就多余了。
好的注釋提供信息、表達(dá)意圖、闡釋、警告
我們經(jīng)常遇到這樣的情況:注釋寫的代碼執(zhí)行邏輯與實(shí)際代碼的邏輯并不符合。大多數(shù)時(shí)候都是因?yàn)榇a變化了,而注釋并沒有跟進(jìn)變化。所以,注釋最好提供一些代碼沒有的額外信息,展示自己的設(shè)計(jì)意圖,而不是寫具體如何實(shí)現(xiàn)。
刪除掉注釋的代碼
git 等版本控制已經(jīng)幫我們記錄了代碼的變更歷史,沒必要繼續(xù)留著過時(shí)的代碼,注釋的代碼也會對閱讀等造成干擾。
錯(cuò)誤處理
錯(cuò)誤處理很重要,但他不能搞亂代碼邏輯
錯(cuò)誤處理應(yīng)該集中在同一層處理,并且錯(cuò)誤處理的函數(shù)最好不包含其他的業(yè)務(wù)邏輯代碼,只需要處理錯(cuò)誤信息即可。
拋出異常時(shí)提供足夠多的環(huán)境和說明,方便排查問題
異常拋出時(shí)最好將執(zhí)行的類名,關(guān)鍵數(shù)據(jù),環(huán)境信息等均拋出,此時(shí)自定義的異常類就派上用場了,通過統(tǒng)一的一層處理異常,可以方便快速地定位到問題。
特例模型可消除異??刂苹蛘?null 判斷
大多數(shù)的異常都是來源于 NPE,有時(shí)候這個(gè)可以通過 Null Object 來消除掉。
盡量不要返回 null ,不要傳 null 參數(shù)
不返回 null 和不傳 null 也是為了盡量降低 NPE 的可能性。
如何判斷不是好的代碼
討論了好代碼的必要條件,我們再來看看好代碼的否定條件:什么不是好的代碼。Kent Beck 使用味道來形容重構(gòu)的時(shí)機(jī),我認(rèn)為當(dāng)代碼有壞味道的時(shí)候,也代表了其并不是好的代碼。
代碼的壞味道
重復(fù)
- 重復(fù)可能是軟件中一切邪惡的根源。—— Robert C.Martin
Martin Fowler 也認(rèn)為壞味道中首當(dāng)其沖的就是重復(fù)代碼。
很多時(shí)候,當(dāng)我們消除了重復(fù)代碼之后,發(fā)現(xiàn)代碼就已經(jīng)比原來整潔多了。
函數(shù)過長、類過大、參數(shù)過長
過長的函數(shù)解釋能力、共享能力、選擇能力都較差,也不易維護(hù)。
過大的類代表了類做了很多事情,也常常有過多的重復(fù)代碼。
參數(shù)過長,不易理解,調(diào)用時(shí)也容易出錯(cuò)。
發(fā)散式變化、霰彈式修改、依戀情結(jié)
如果一個(gè)類不是單一職責(zé)的,則不同的變化可能都需要修改這個(gè)類,說明存在發(fā)散式變化,應(yīng)考慮將不同的變化分離開。
如果某個(gè)變化需要修改多個(gè)類的方法,則說明存在霰彈式修改,應(yīng)考慮將這些需要修改的方法放入同一個(gè)類。
如果函數(shù)對于某個(gè)類的興趣高于了自己所處的類,說明存在依戀情結(jié),應(yīng)考慮將函數(shù)轉(zhuǎn)移到他應(yīng)有的類中。
數(shù)據(jù)泥團(tuán)
有時(shí)候會發(fā)現(xiàn)三四個(gè)相同的字段,在多個(gè)類和函數(shù)中均出現(xiàn),這時(shí)候說明有必要給這一組字段建立一個(gè)類,將其封裝起來。
過多的 if…else 或者使用 switch
過多的 if…else 或者 switch ,都應(yīng)該考慮用多態(tài)來替換掉。甚至有些人認(rèn)為除個(gè)別情況外,代碼中就不應(yīng)該存在 if…else 。
總結(jié)
本文首先一句話概括了我認(rèn)為的好代碼的必要條件:整潔,接著具體分析了整潔代碼的特點(diǎn),又分析了好代碼的否定條件:什么樣的代碼不是好的代碼。僅是本人的一些見解,希望對各位以后的編程有些許的幫助。
我認(rèn)為僅僅編寫出可運(yùn)行的代碼是遠(yuǎn)遠(yuǎn)不夠的,還要時(shí)刻注意代碼的整潔度,留下一些漂亮的代碼,希望寫的代碼都能保留并運(yùn)行 102 年!