如何提高代碼品味
一家之言,可以在評論里探討
寫代碼雖然大多數(shù)時候是個體力活,但不可否認,也需要一點品位。我曾經(jīng)覺得代碼質(zhì)量很重要,后來寫業(yè)務(wù)寫多了,又覺得如果連代碼正確都做不到,又談何代碼質(zhì)量。后來我又醒悟了,這世上很難有 bug free 的代碼,當(dāng)出現(xiàn) bug
的時候,好代碼比爛代碼會好改很多。我們今天就討論下什么是好代碼,畢竟一個不知道什么樣的代碼是好代碼的人是不可能如有神助寫出好代碼的,寫代碼可以搜索復(fù)制黏貼三板斧,寫好代碼卻是必須刻意練習(xí)的。
什么是寫代碼
我覺得寫代碼分為兩個部分:
- 結(jié)構(gòu)設(shè)計,包括模塊劃分、模塊交互、接口設(shè)計
- 功能實現(xiàn),涉及具體的語言特性以及代碼風(fēng)格
結(jié)構(gòu)設(shè)計
所謂的結(jié)構(gòu)設(shè)計不是說一定要畫個架構(gòu)圖,寫個系分文檔什么的,結(jié)構(gòu)設(shè)計和功能實現(xiàn)其實螺旋貫穿在整個寫代碼的過程中。當(dāng)我們準(zhǔn)備完成一個需求的時候,會把需求分成幾個功能,這些功能如果互相獨立,便不涉及交互,否則他們之間就需要溝通,可能是直接調(diào)用,可能是發(fā)送消息,可能是監(jiān)聽變化,可能是輪詢結(jié)果等等。分了功能之后,要實現(xiàn)其中某個功能,又要遞歸的執(zhí)行一遍上述過程,直到寫下一行行代碼。有同學(xué)可能覺得這種自頂向下的過程太宏觀了,前期太費時間,什么模塊什么交互,我就挑個功能一把梭,代碼先寫起來。這當(dāng)然也可以,而且大多數(shù)人都是這么做的。但這其實也包含結(jié)構(gòu)設(shè)計,你準(zhǔn)備率先實現(xiàn)的那個功能,潛意識里你已經(jīng)把它從整個系統(tǒng)中分離出來了,只是系統(tǒng)的其他部分暫時先不管而已。模塊劃分是個說爛的話題,但它又真的是軟件工程的精髓,它的意義在于,人管理復(fù)雜度的能力是有限的,當(dāng)一大坨代碼懟在一起的時候,哪怕代碼質(zhì)量再高,注釋再詳盡,也會引起生理上的不適。這種不適最容易發(fā)生在當(dāng)你要修改一個小功能,找了半天代碼找不到的時候。劃分了之后,哪怕是好幾坨爛代碼,但你改動的時候只改其中一個文件,其他代碼也是眼不見心不煩。
那模塊如何劃分?我們可以說出一些普適的原則,譬如高內(nèi)聚低耦合、單一職責(zé)原則、開閉原則等等,但這些東西說起來感覺很套路很不真誠,讓人覺得無從下手。我個人覺得有兩條很重要的原則:
- 開發(fā)的過程中時刻反思自己的代碼結(jié)構(gòu)
- 認真命名
關(guān)于反思代碼結(jié)構(gòu),最重要的當(dāng)然是自己的思考,不要迷信別人給你做的設(shè)計,也不要迷信自己當(dāng)初的設(shè)計,我大概列舉幾種情況:
- 一開始分了兩個模塊,寫著寫著發(fā)現(xiàn)其中一個模塊的體積很大,那就看看能不能再繼續(xù)分
- 一開始分了四五個模塊,后來寫著寫著發(fā)現(xiàn)其中兩個模塊交互巨頻繁,他們必須一起配合才能實現(xiàn)一個完整的功能,那就把他們合在一起(高內(nèi)聚)
- 在迭代的過程中發(fā)現(xiàn)兩個模塊雖然相互獨立,但交互邏輯寫得很死,依賴關(guān)系很直接,修改了一個,另一個也必須改,那就修改他們的依賴和交互,盡量做到互不影響(松耦合)
- 在迭代過程中發(fā)現(xiàn)兩個模塊的某部分是可以共用的,那就抽出來(DRY)
- 在迭代過程中發(fā)現(xiàn)某個模塊的一段代碼變更很頻繁,那就單獨把這部分抽出來(封裝變化)
- 。。。
這些情況當(dāng)然列舉不完,團隊中其實可以定一些硬性指標(biāo)來輔助模塊劃分,譬如一個文件最多 300 行,一個函數(shù)最多 70 行什么的,放在 Lint 規(guī)則里。我們有很多約定俗成的“潛規(guī)則”其實都有它背后的邏輯,譬如以前天天說的 MVC 和 MVVM 兩個模式,他們的最主要區(qū)別不在于模塊劃分,而是模塊間的交互,在劃分方面它們都致力于讓 UI 和邏輯分開,為什么呢?因為 UI is cheap,UI 是隔三差五會被設(shè)計師推翻再來一套的,但邏輯和數(shù)據(jù),相對會穩(wěn)定一點,所以把他們分開,UI 迭代的時候涉及的改動就比較小。那為什么前端的 React/Vue
這些近年大火的框架,又提倡所謂的組件(Component),貌似 是要把 UI 和邏輯搞在一起呢?其實不是的,組件是一個小粒度的模塊,它相對獨立,具有很高的內(nèi)聚性,組件中的“邏輯”更多的應(yīng)該是 UI 相關(guān)的交互邏輯,而不是那些比較底層的邏輯,我們還是應(yīng)該把相對穩(wěn)定的邏輯抽出來放到更下層。
關(guān)于命名,很多同學(xué)可能不在意,覺得代碼能跑就行了,取名字有什么關(guān)系,看不懂我加注釋嘛。這是非常不好的習(xí)慣,因為命名的過程中其實就是在概括你這段代碼,如果你的某個函數(shù)名叫 xxxAndxxx
那這個函數(shù)就應(yīng)該被拆成兩個函數(shù),它明顯違反了單一職責(zé)原則。大到業(yè)務(wù)模塊小到輔助函數(shù),只要你覺得不好命名,那就是一個信號,說明這段代碼做的事情太多太雜,以至于你無法用幾個單詞概況出來。
功能實現(xiàn)
現(xiàn)在我們具體聊聊代碼實現(xiàn)的時候怎么體現(xiàn)代碼品位,我覺得主要可以從三點著手提高:
- 精通你所用的編程語言
- 提高自己的邏輯能力
- 注重代碼風(fēng)格
有一個廣為人知的觀點,編程思路最重要,語言只是工具。乍一聽,編程語言似乎無足輕重。如果只是一錘子買賣,寫段代碼實現(xiàn)個功能,寫完離手,再不相干,那語言當(dāng)然不重要。我相信大多數(shù)碼農(nóng)都可以很快上手一個新語言,因為要實現(xiàn)一個功能可能只需要一些通用的核心特性,對 C 系語言來說,知道函數(shù)/分支語句/循環(huán)語句/字符串/數(shù)組/散列表這些東西的使用就足夠開發(fā)日常需求了,而這些特性說實話在 C
系語言中都大同小異。但如果要寫好代碼,就得向著精通這門語言努力。有些功能你寫一堆蹩腳代碼可能實現(xiàn)得馬馬虎虎,但用了某個特性,幾行短小精悍的代碼就解決了。我很排斥一個觀點是,為了讓團隊的所有人都能看懂,鼓勵只使用語言的基本特性,一些高級的或者不太常用的特性不準(zhǔn)用,用了就是炫技。舉個極端點的例子,有人覺得 if else 比三元表達式更可讀,就鼓勵只使用 if else。這樣的“可讀”在我看來只是迎合平庸的碼農(nóng)。有些語言特性確實有利有弊,有的甚至只有弊(JS
中就很常見),那盡量不用,或者根據(jù)實際場景做取舍。我們?nèi)∩岬臉?biāo)準(zhǔn)是“場景”,而不應(yīng)該是人。什么叫合適的場景呢,還是拿三元表達式舉例:
const data1 = a > b ? 100 : 200;
const data2 = a > b ? 300 : 400;
我看到過有同學(xué)這樣用,這段代碼雖然只有兩行,但 a > b 這個條件要判斷兩次,性能我們且不論(a > b 只是個示例,實際的條件可能更復(fù)雜),至少已經(jīng)重復(fù)了,寫的時候?qū)憙纱危x的人也要看兩次,不如就
let data1 = 200;
let data2 = 400;
if (a > b) {
data1 = 100;
data2 = 300;
}
或者把 a > b 這個 condition 提前計算好,避免計算兩次(這種優(yōu)化不是針對性能,因為有些語言編譯器在編譯期會做類似這種優(yōu)化,主要還是可讀性):
const condition = a > b;
const data1 = condition ? 100 : 200;
const data2 = condition ? 300 : 400;
代碼要簡潔,但不是簡短(代碼行數(shù)少),簡潔的意思是邏輯清晰,沒有冗余信息。還有一個場景是我有看過同學(xué)用嵌套的三元表達式來替代多個 if else 的操作,那真的是可讀性很差了,不要這樣。
這方面也有一些具體的技巧,譬如我個人很喜歡用散列表去代替分支語句:
// if (key === 'x') return 'xxx';
// if (key === 'y') return 'yyy';
// if (key === 'z') return func;
const xxxMap = {
x: 'xxx',
y: 'yyy',
z: func
};
return xxxMap[key];
這種做法好像有個名字叫“表驅(qū)動設(shè)計”,這樣做有很多好處,代碼簡潔是一點,這個 xxxMap 其實是一張配置表,以后可以抽出去放到單獨的地方,甚至放到服務(wù)端去配置,就可以很容易實現(xiàn)一些動態(tài)化需求。更延伸開去講,能配置化的東西盡量都配置化,方便以后擴展功能。
再舉個 reduce 的例子,如何根據(jù)場景選擇:
const reservedWords = ['initialState', 'state', 'effects', 'actions', 'updates', 'mutations'];
// 這里其實用 reduce 會好看些:
// store => reservedWords.reduce((acc, x) => acc || store.hasOwnProperty(x), false)
// 但為了能提前返回,性能稍微好一點,還是用了 for
const isSingleStore = (store) => {
for (let i in reservedWords) {
if (store.hasOwnProperty(reservedWords[i])) return true;
}
return false;
};
說了語言重要,那思路呢?當(dāng)然也重要。我們?nèi)粘I钪校壿嬊逦娜巳詢烧Z就直擊要害,邏輯混亂的人兜兜轉(zhuǎn)轉(zhuǎn)還是云里霧里。代碼也一樣,有些糟糕的代碼是糟糕在啰嗦,一個判斷能搞定的事情它可能要變著法判斷兩三次。這種呢,就是本身思路不簡潔,寫出來的代碼自然也簡潔不了,只能努力提高自己的邏輯能力。
剩下的代碼風(fēng)格,也非常重要。長得好看的人是有特權(quán)的,長得好看的代碼自然也有。多看看官方或者大廠的 Style Guide,平常多注意空格換行縮進命名風(fēng)格等等,裝個優(yōu)秀的格式化插件也好。
差不多就先這樣吧,品味不見得有好壞,但有高低,共勉。