如何提高程式碼品味
如何提高程式碼品味
一家之言,可以在評論裡探討
寫程式碼雖然大多數時候是個體力活,但不可否認,也需要一點品位。我曾經覺得程式碼質量很重要,後來寫業務寫多了,又覺得如果連程式碼正確都做不到,又談何程式碼質量。後來我又醒悟了,這世上很難有 bug free 的程式碼,當出現 bug
的時候,好程式碼比爛程式碼會好改很多。我們今天就討論下什麼是好程式碼,畢竟一個不知道什麼樣的程式碼是好程式碼的人是不可能如有神助寫出好程式碼的,寫程式碼可以搜尋複製黏貼三板斧,寫好程式碼卻是必須刻意練習的。
什麼是寫程式碼
我覺得寫程式碼分為兩個部分:
- 結構設計,包括模組劃分、模組互動、介面設計
- 功能實現,涉及具體的語言特性以及程式碼風格
結構設計
所謂的結構設計不是說一定要畫個架構圖,寫個系分文件什麼的,結構設計和功能實現其實螺旋貫穿在整個寫程式碼的過程中。當我們準備完成一個需求的時候,會把需求分成幾個功能,這些功能如果互相獨立,便不涉及互動,否則他們之間就需要溝通,可能是直接呼叫,可能是傳送訊息,可能是監聽變化,可能是輪詢結果等等。分了功能之後,要實現其中某個功能,又要遞迴的執行一遍上述過程,直到寫下一行行程式碼。有同學可能覺得這種自頂向下的過程太巨集觀了,前期太費時間,什麼模組什麼互動,我就挑個功能一把梭,程式碼先寫起來。這當然也可以,而且大多數人都是這麼做的。但這其實也包含結構設計,你準備率先實現的那個功能,潛意識裡你已經把它從整個系統中分離出來了,只是系統的其他部分暫時先不管而已。模組劃分是個說爛的話題,但它又真的是軟體工程的精髓,它的意義在於,人管理複雜度的能力是有限的,當一大坨程式碼懟在一起的時候,哪怕程式碼質量再高,註釋再詳盡,也會引起生理上的不適。這種不適最容易發生在當你要修改一個小功能,找了半天程式碼找不到的時候。劃分了之後,哪怕是好幾坨爛程式碼,但你改動的時候只改其中一個檔案,其他程式碼也是眼不見心不煩。
那模組如何劃分?我們可以說出一些普適的原則,譬如高內聚低耦合、單一職責原則、開閉原則等等,但這些東西說起來感覺很套路很不真誠,讓人覺得無從下手。我個人覺得有兩條很重要的原則:
- 開發的過程中時刻反思自己的程式碼結構
- 認真命名
關於反思程式碼結構,最重要的當然是自己的思考,不要迷信別人給你做的設計,也不要迷信自己當初的設計,我大概列舉幾種情況:
- 一開始分了兩個模組,寫著寫著發現其中一個模組的體積很大,那就看看能不能再繼續分
- 一開始分了四五個模組,後來寫著寫著發現其中兩個模組互動巨頻繁,他們必須一起配合才能實現一個完整的功能,那就把他們合在一起(高內聚)
- 在迭代的過程中發現兩個模組雖然相互獨立,但互動邏輯寫得很死,依賴關係很直接,修改了一個,另一個也必須改,那就修改他們的依賴和互動,儘量做到互不影響(鬆耦合)
- 在迭代過程中發現兩個模組的某部分是可以共用的,那就抽出來(DRY)
- 在迭代過程中發現某個模組的一段程式碼變更很頻繁,那就單獨把這部分抽出來(封裝變化)
- 。。。
這些情況當然列舉不完,團隊中其實可以定一些硬性指標來輔助模組劃分,譬如一個檔案最多 300 行,一個函式最多 70 行什麼的,放在 Lint 規則裡。我們有很多約定俗成的“潛規則”其實都有它背後的邏輯,譬如以前天天說的 MVC 和 MVVM 兩個模式,他們的最主要區別不在於模組劃分,而是模組間的互動,在劃分方面它們都致力於讓 UI 和邏輯分開,為什麼呢?因為 UI is cheap,UI 是隔三差五會被設計師推翻再來一套的,但邏輯和資料,相對會穩定一點,所以把他們分開,UI 迭代的時候涉及的改動就比較小。那為什麼前端的 React/Vue
這些近年大火的框架,又提倡所謂的元件(Component),貌似 是要把 UI 和邏輯搞在一起呢?其實不是的,元件是一個小粒度的模組,它相對獨立,具有很高的內聚性,元件中的“邏輯”更多的應該是 UI 相關的互動邏輯,而不是那些比較底層的邏輯,我們還是應該把相對穩定的邏輯抽出來放到更下層。
關於命名,很多同學可能不在意,覺得程式碼能跑就行了,取名字有什麼關係,看不懂我加註釋嘛。這是非常不好的習慣,因為命名的過程中其實就是在概括你這段程式碼,如果你的某個函式名叫 xxxAndxxx
那這個函式就應該被拆成兩個函式,它明顯違反了單一職責原則。大到業務模組小到輔助函式,只要你覺得不好命名,那就是一個訊號,說明這段程式碼做的事情太多太雜,以至於你無法用幾個單詞概況出來。
功能實現
現在我們具體聊聊程式碼實現的時候怎麼體現程式碼品位,我覺得主要可以從三點著手提高:
- 精通你所用的程式語言
- 提高自己的邏輯能力
- 注重程式碼風格
有一個廣為人知的觀點,程式設計思路最重要,語言只是工具。乍一聽,程式語言似乎無足輕重。如果只是一錘子買賣,寫段程式碼實現個功能,寫完離手,再不相干,那語言當然不重要。我相信大多數碼農都可以很快上手一個新語言,因為要實現一個功能可能只需要一些通用的核心特性,對 C 系語言來說,知道函式/分支語句/迴圈語句/字串/陣列/散列表這些東西的使用就足夠開發日常需求了,而這些特性說實話在 C
系語言中都大同小異。但如果要寫好程式碼,就得向著精通這門語言努力。有些功能你寫一堆蹩腳程式碼可能實現得馬馬虎虎,但用了某個特性,幾行短小精悍的程式碼就解決了。我很排斥一個觀點是,為了讓團隊的所有人都能看懂,鼓勵只使用語言的基本特性,一些高階的或者不太常用的特性不準用,用了就是炫技。舉個極端點的例子,有人覺得 if else 比三元表示式更可讀,就鼓勵只使用 if else。這樣的“可讀”在我看來只是迎合平庸的碼農。有些語言特性確實有利有弊,有的甚至只有弊(JS
中就很常見),那儘量不用,或者根據實際場景做取捨。我們取捨的標準是“場景”,而不應該是人。什麼叫合適的場景呢,還是拿三元表示式舉例:
const data1 = a > b ? 100 : 200; const data2 = a > b ? 300 : 400;
我看到過有同學這樣用,這段程式碼雖然只有兩行,但 a > b 這個條件要判斷兩次,效能我們且不論(a > b 只是個示例,實際的條件可能更復雜),至少已經重複了,寫的時候寫兩次,讀的人也要看兩次,不如就
let data1 = 200; let data2 = 400; if (a > b) { data1 = 100; data2 = 300; }
或者把 a > b 這個 condition 提前計算好,避免計算兩次(這種優化不是針對性能,因為有些語言編譯器在編譯期會做類似這種優化,主要還是可讀性):
const condition = a > b; const data1 = condition ? 100 : 200; const data2 = condition ? 300 : 400;
程式碼要簡潔,但不是簡短(程式碼行數少),簡潔的意思是邏輯清晰,沒有冗餘資訊。還有一個場景是我有看過同學用巢狀的三元表示式來替代多個 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];
這種做法好像有個名字叫“表驅動設計”,這樣做有很多好處,程式碼簡潔是一點,這個 xxxMap 其實是一張配置表,以後可以抽出去放到單獨的地方,甚至放到服務端去配置,就可以很容易實現一些動態化需求。更延伸開去講,能配置化的東西儘量都配置化,方便以後擴充套件功能。
再舉個 reduce 的例子,如何根據場景選擇:
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; };
說了語言重要,那思路呢?當然也重要。我們日常生活中,邏輯清晰的人三言兩語就直擊要害,邏輯混亂的人兜兜轉轉還是雲裡霧裡。程式碼也一樣,有些糟糕的程式碼是糟糕在囉嗦,一個判斷能搞定的事情它可能要變著法判斷兩三次。這種呢,就是本身思路不簡潔,寫出來的程式碼自然也簡潔不了,只能努力提高自己的邏輯能力。
剩下的程式碼風格,也非常重要。長得好看的人是有特權的,長得好看的程式碼自然也有。多看看官方或者大廠的 Style Guide,平常多注意空格換行縮排命名風格等等,裝個優秀的格式化外掛也好。
差不多就先這樣吧,品味不見得有好壞,但有高低,共勉。