在我們的程式設計開發中,如果能在沒有一行註釋的程式碼中找到註釋,是不是很意思呢?
我們經常容易犯一個錯誤:我們修改了一段程式碼,但是忘記修改更新註釋。混亂的註釋並不會打斷你程式碼的執行,但是想象一下debug的時候會發生什麼事情。你認真地閱讀了註釋,它說的是一件事,但是程式碼乾的是另一件事。結果是,你浪費了很多時間發現註釋是錯誤的,甚至最糟糕的是,你可能完全被誤導了。
但是程式碼中完全不寫註釋也不是一個很好地選擇。在我超過15年的程式設計經驗中,我從未覺得程式碼庫中的註釋是完全沒用的。
然而,有一些辦法可以來幫助減少程式碼註釋的必要性。我們可以利用某些編碼技巧來讓我們的程式碼變得更清晰,例如就是利用程式語言的特點。這樣不僅能幫我們的程式碼變得更加清晰易理解,而且還能能幫助我們改善程式的設計。
這類程式碼通常被稱為自文件化程式碼。讓我來給大家展示下怎樣利用這種方式編碼。當然,我這裡我演示的程式碼使用的是JavaScript語言,你們也可以利用這裡大部分的技巧運用到其他的語言中。
概述
一些程式設計者將註釋歸納到自文件化程式碼的範疇。在這篇文章中,我們只關注程式碼。註釋很重要,但是卻單獨覆蓋了太多東西。我們可以把自文件化程式碼歸為三大類:
- 結構類自文件化。使用程式碼的結構和目錄來讓程式碼變得清晰
- 命名自文件化。例如函式或變數命名讓程式碼更易理解
- 語句相關自文件化。我們利用語言的特性來讓程式碼變得清晰
一、結構類自文件化
首先我們看下結構類自文件化。這裡指的是通過移動部分程式碼來讓程式碼變得清晰。
- 將程式碼移動到函式裡面。
這和提取函式重構一樣,意思是我們將已經寫好的程式碼移動到一個新的函式裡:即提取程式碼成為一個新函式。例如:
1 |
var width = (value - 0.5) * 16; |
不是很清晰,這裡新增一個註釋會很有幫助,或者,我們將它提取到一個函式裡面進行自文件描述:
1 2 3 4 5 |
var width = emToPixels(value); function emToPixels(ems) { return (ems - 0.5) * 16; } |
這裡唯一的變化是我們把計算放到函式裡面。函式的名稱描述了程式碼的作用,所以程式碼的意思就不言而喻了。另一個好處是,我們現在有一個到處可以呼叫的輔助函式,所以這種方法也增強了程式碼複用性。
- 用函式代替條件表示式
如果沒有註釋,含有多個操作運算的語句很難理解。我們可以使用一個簡單的方法來描述。
1 2 |
if(!el.offsetWidth || !el.offsetHeight) { } |
這段程式碼的目的是啥?
1 2 3 4 5 6 |
function isVisible(el) { return el.offsetWidth && el.offsetHeight; } if(!isVisible(el)) { } |
同樣,我們把程式碼移動到一個函式裡面,程式碼就立即變得容易理解了。
- 使用變數代替表示式
使用變數代替表示式和將程式碼移入函式裡面類似,但是相對於函式,我們是用一個變數。讓我們再來看下這個語句:
1 2 |
if(!el.offsetWidth || !el.offsetHeight) { } |
這次我們用變數來代替表示式,而不是函式:
1 2 3 |
var isVisible = el.offsetWidth && el.offsetHeight; if(!isVisible) { } |
這樣比使用函式更好,例如,如果使用的邏輯非常明確,而且這段邏輯只在某一個地方使用一次,就適合用變數了。這種方法最常用的場景是在數學表示式中:
1 |
return a * b + (c / d); |
我們可以這樣讓程式碼變得清晰:
1 2 3 |
var multiplier = a * b; var divisor = c / d; return multiplier + divisor; |
因為我數學很差,想象一下上面的例子是一個有含義的運算,在任何場景下,你可以將複雜的表示式換成變數來給表示式程式碼新增本身的含義。
- 類和模組介面
介面不僅是類或模組的公用方法和屬性,而且在使用中能起到自文件化的作用。來看下這個例子:
1 2 3 4 5 6 7 8 9 |
class Box { setState(state) { this.state = state; } getState() { return this.state; } } |
這個類當然可以含有其他的程式碼部分,我這裡是讓例子簡單些來演示公共介面是怎樣自文件化的。你能理解這個類是怎樣使用的嗎?可能需要通過一段時間的理解,但不是特別明顯。
這兩個函式都有合理的命名:他們做的事情從它們的名稱來看非常清晰。但是儘管如此,你仍然不知道如何使用它們。很可能你需要閱讀更多的程式碼或文件來理解怎麼用。所以我們可以這樣改下:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Box { open() { this.state = 'open'; } close() { this.state = 'closed'; } isOpen() { return this.state === 'open'; } } |
很容易理解了用法,對吧?需要注意的是,我們只改變了公用介面,內部的宣告仍然使用的是this.state
來用的。現在你可以一眼就知道Box
類是怎樣來使用的了。這就演示了儘管使用了很好命名,但是整個模組使用仍然不好理解的情況,通過這些簡單的決策,你會有一個更好的認知。
- 程式碼分塊。
對不同部分的程式碼進行分塊也能達到自文件化的目的。例如,你應該養成將變數的宣告和它們使用的語句放到一起的習慣,並儘可能將變數的使用放到一起。這樣可以顯示不同部分程式碼之間的聯絡,所以後面任何人修改都能花更少的時間來找到它被使用到的所有位置。看下這個例子:
1 2 3 4 5 6 7 8 |
var foo = 1; blah() xyz(); bar(foo); baz(1337); quux(foo); |
你能一眼看出foo
被呼叫了幾次嗎?再來看下這個例子:
1 2 3 4 5 6 7 8 |
var foo = 1; bar(foo); quux(foo); blah() xyz(); baz(1337); |
通過把所有的foo
呼叫的地方放到一起,我們可以很容易的看到那幾個部分的方法函式式依賴它的。
- 使用純函式。
純函式比普通的函式更容易理解,什麼是純函式?當使用同樣的引數呼叫,如果輸出的內容相同,這就是所謂的”純函式”。這就是說,這個函式不會對狀態有任何依賴或影響,例如時間、物件屬性、ajax方法等等。這類函式很容易理解,因為很多影響他們輸出結果的值都被明確指定了,你不用研究某個東西到底是從哪裡來的,或者什麼東西會影響輸出,而是很直觀能理解到的。
這類函式能夠進行自文件化描述的另一個原因是你可以信任它們的輸出。無論怎樣,這種函式會根據你的輸入返回特定的結果。它不會影響任何額外的東西,所以你能確定它不會帶來任何副影響。
一個這類函式出錯的例子是document.write()
,有經驗的js開發者知道我們不應該使用它,但是很多新手還在那折騰。有時它沒問題,但是有時,在某些特定情況,它會清除頁面所有內容。這就是副影響。
為了瞭解什麼是純函式,可以看下這篇文章《Functional Programming: Pure Functions》。
- 目錄和檔案結構
在檔案或目錄命名時,根據專案中的命名規範進行命名。如果沒有明確的規範,那就根據你語言選擇合適的標準。例如:如果你新增一個與UI相關的程式碼,發現專案中有相似功能的內容,如果UI相關的內容放在src/ui/
的資料夾下,你應該也這樣做。這樣可以更容易的找到程式碼,並且根據專案中其它的程式碼,你可以很容易知道它的作用。所有的UI程式碼都在相同的地方,然後,它必須是與UI相關的。
二、命名自文件化
電腦科學領域有句名言:
在計算機凌雲有量大難題:快取失效和命名。–Phil Karlton
所以讓我們來看下我們怎樣使用命名來使我們的程式碼自文件化。
- 重新命名函式。
函式命名常常比較複雜,但是有一些簡單的規則我們可以參考:
1、避免使用想handle或manage這類單詞:handleObject,manageObject。輕微這些什麼意思。
2、使用主動詞:cutGruss(),sendFile。根據函式的具體功能來決定。
3、使用返回值:getMagicBullet(),readFile。這種情況並不經常使用,但是在語義化方面很有作用
4、使用強型別語言編碼能明確知道返回值是什麼型別
- 重新命名變數
對於變數,有兩個比較好的方法。
1、指明單位:如果你含有數字引數,你可以帶上單位。例如widthPx
就比 width
要好。 2、不要使用簡寫:例如a和b,這些是不能理解的變數,除非是在迴圈計數裡面。
- 遵循已有的命名規範
程式碼中儘量使用原有的規範。例如,如果你有一個具體型別的物件,那麼使用相同的名字:
1 |
var element = getElement(); |
千萬不要傻傻的這樣寫:
1 |
var node = getElement(); |
如果你遵循其它地方程式碼的規範,任何一個讀程式碼的人都能正確的認為某個東西在任何地方出現的含義都是相同的。
- 使用有含義的錯誤
Undefined is not an object。很多人都喜歡這樣提示到頁面上,所以讓我們提示一些具有提示意義的內容給頁面使用者。怎樣讓錯誤變得有含義:
1、應該描述具體問題所在。 2、如果可能,應該包含是哪個變數或資料導致的問題。 3、關鍵一點:錯誤應該能幫助我們找到哪裡出了問題。例如錯誤上報的方式收集解決問題。
三、 宣告類自文件化
讓我們看下一個JavaScript的例子:
1 |
imTricky && doMagic(); |
儘量這樣寫:
1 2 3 |
if(imTricky) { doMagic(); } |
宣告技巧並不適合每個人。
- 使用命名常量。例如
const MEANING_OF_LIFE = 42;
- 避免bool值。例如
myThing.setData({ x: 1 }, true);
- 使用程式語言的優勢。
我們甚至可以使用程式語言某些特有的特性來描述一段程式碼的作用:
1 2 3 4 |
var ids = []; for(var i = 0; i < things.length; i++) { ids.push(things[i].id); } |
上面程式碼收集了一個陣列元素裡面的id。但是我麼可以這樣寫。
1 2 3 |
var ids = things.map(function(thing) { return thing.id; }); |
在這個例子中。我們立即知道了這個過程的作用,因為這就是map方法的目的。另一個JavaScript的例子是const
關鍵字,常常,我們通過它來宣告不會更變的變數。一個非常通用的例子是載入CommonJS模組的時候:
1 |
var async = require('async'); |
我們可以描述得更清晰:
1 |
const async = require('async'); |
另一個好處是,如果有人修改,會報錯。
四、反模式
當這些你都熟悉了之後,你可以做更多的事情,然而有些事情你必須要小心:
- 提取更短的函式。
一些人主張使用很小的函式,將所有的東西函式化,你可以這樣做,但是,這會增加程式碼理解的難度。例如,試想一下,你debug的時候,你看了A函式,然後A呼叫了B函式,然後B呼叫了C函式…短的函式比較容易理解,但是如果你只是在一個地方使用,推薦使用變數來代替。
- 不要強迫去使用
通常,沒有絕對正確的方式,所以,如果聽起來某件事情很不錯,但是不要強制自己去使用。
結論
讓自己的程式碼自文件化是一件是你的程式碼更容易維護的事情,也是一件很難的事情。每一行新增的程式碼註釋不一定會被維護起來,所以消除程式碼註釋也是一件很好的事情。
然而,自文件化程式碼並不能代替文件或註釋。例如,程式碼在表達方面是有限的,所以你也需要很好的註釋來補充。API文件對於程式碼庫來說是很重要的,除非你的程式碼很小,否則自文件化的方式並不可行。