在代碼裏面找到一個徹底沒有地方或沒有用的註釋是否是頗有趣?node
這是一個很容易犯的錯誤:你改變了一些代碼,但忘記刪除或更新註釋。壞的註釋不會破壞你的代碼,但你能夠想象一下調試時會發生什麼。你讀了註釋,但代碼卻在作另外一件事,也許最終你浪費了一些時間來弄懂它,甚至最壞的狀況是,它誤導了你。程序員
但沒有編寫任何註釋的代碼不是一個選擇。在我超過15年的編程經驗裏,我歷來沒有見過一個代碼庫,其中的評論是徹底沒必要要的。算法
註釋不只有助於使咱們的代碼更容易理解,也能夠幫助咱們改進整個程序的設計。express
這種類型的編碼叫作自我文檔化,如今讓我來告訴你如何採用這種方式編程。雖然在這裏個人例子使用的是JavaScript,但你能夠應用到期其餘的語言和技術中去。編程
一些程序員把註釋做爲代碼自我文檔化的一部分,在本文中,咱們只關注代碼,註釋當然很重要,但它是一個須要單獨討論的大話題。數組
咱們能夠將代碼自我文檔化的技術分爲3大類:安全
structural(結構):其中代碼或目錄的結構用於闡明目的async
naming related(命名相關):例如函數或變量的命名函數
syntax related(語法相關):咱們利用(或者避免使用)語言的特徵來是代碼清晰ui
其中不少是看起來很簡單,挑戰來自於你要知道何時用什麼技術。我會告訴你一些實際例子,咱們將會處理的每個例子。
首先,咱們來看一下結構類別。結構變化時爲了加強代碼的清晰度而移動代碼。
這與提取代碼
重構相同——意味着咱們採用現有代碼並將其移動到一個新的函數中:咱們將代碼提取
到一個新函數中。
例如,猜測一下下面的代碼是作什麼的:
var width = (value - 0.5) * 16;
上述代碼不是很清楚,此時註釋多是很是有用的,但或許咱們能夠提取一個函數,使其自我文檔化:
var width = emToPixels(value); function emToPixels(ems) { return (ems - 0.5) * 16; }
惟一的變化時我把計算移動到一個函數裏,函數的名稱描述了它的做用,因此代碼再也不須要註釋。做爲一個額外的好處,咱們如今有了一個有用的函數,咱們能夠在其餘地方使用這個函數,這種方法有助於減小代碼重複冗餘。
不少時候帶有多個操做數的代碼,若是沒有註釋是很難理解的。咱們能夠應用相似上述的方法來使代碼清晰:
if(!el.offsetWidth || !el.offsetHeight) { }
上訴條件的目的是什麼?
function isVisible(el) { return el.offsetWidth && el.offsetHeight; } if(!isVisible(el)) { }
再一次,咱們把代碼移動到一個函數內,代碼當即更容易理解。
用變量替換某個東西相似於將代碼移動到一個函數中,而不是一個函數,此時咱們只須要一個變量。
讓咱們再看一下if條件語句的例子:
if(!el.offsetWidth || !el.offsetHeight) { }
咱們還能夠經過引入一個變量,而不是提取一個函數,來使咱們的代碼自我文檔化:
var isVisible = el.offsetWidth && el.offsetHeight; if(!isVisible) { }
這多是比提取函數更好的選擇,例如,當你當你想要闡明的邏輯對於僅在一個地方使用的某個算法很是特定時。
這種方法最多見的是用於數字表達式:
return a * b + (c / d);
咱們能夠經過分割計算來使上述代碼更清晰:
var multiplier = a * b; var divisor = c / d; return multiplier + divisor;
由於我懼怕數學,想象上述的例子仍是有一些算法的。在任何狀況下,代碼自我文檔化的關鍵是你能夠將複雜的表達式移動到變量中,並增長意義,不然你的代碼是難以的理解的。
類和模塊的接口——即公共方法和屬性,能夠做爲其使用的文檔。
讓咱們來看一下這個例子:
class Box { setState(state) { this.state = state; } getState() { return this.state; } }
這個類能夠包含一些其餘的代碼。我故意保持示例簡單,以說明公共接口是如何自我文檔化的。
你能告訴我應該如何使用這個類嗎?也許有一點點做用,但它不明顯。
這兩個函數都有合理的名字:它們要作的是闡明本身的名字。可是儘管如此,它不是很清楚你應該如何使用它們,極可能你須要閱讀更多的代碼或類的文檔來弄清楚。
若是咱們把它改爲這樣:
class Box { open() { this.state = 'open'; } close() { this.state = 'closed'; } isOpen() { return this.state === 'open'; } }
這是更容易理解的用法,你不以爲嗎?注意咱們只是改變了公共接口,內部表示仍然與this.satte
屬性相同。
如今你能夠一眼就看出Box類是如何使用的了。這代表這代表即便第一個版本的函數具備良好的名稱,但完整的包仍然是混亂的,如何經過這樣簡單的變化,你能夠有一個很是大的影響。不少時候你須要想一想大局。
代碼分組的不一樣部分也能夠做爲一種文檔形式。
例如,你應該將變量聲明儘量地靠近它們被使用的位置,並嘗試將變量使用組合在一塊兒。
這能夠用於指示代碼不一樣部分之間的關係,以便未來更改它的任何人均可以更容易地找到他們須要查閱的部分。
思考以下的例子:
var foo = 1; blah() xyz(); bar(foo); baz(1337); quux(foo);
你能一眼看出foo
被調用了多少次嗎?對比下面的例子:
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相關的代碼。
這裏有一個流行的摘引關於計算機科學的兩個艱難的方面:
There are only two hard things in Computer Science: cache invalidation and naming things. — Phil Karlton
那麼,讓咱們來談談如何使用合理的命名來使咱們的代碼自我文檔化。
函數的命名通常不太難,這裏有一些簡單的規則,你能夠遵循:
避免使用handle
或manage
這樣的模糊詞:handleLinks()
, manageObjects()
,這些都作了什麼?
使用主動性動詞:cutGrass()
, sendFile()
函數積極地執行了某事
代表返回值:getMagicBullet()
, readFile()
,這不是你老是能夠作到的,但賦予它意義是有幫助的
強類型的語言可使用類型命名來幫助代表返回值
對於變量,這裏有兩個好的經驗法則:
代表單位:若是有數字參數,能夠包含參數的預期單位。例如,widthPX
而不是width
代表值得單位是像素而不是其餘單位
不要使用快捷方式:a
或b
不是可接受的變量名稱, 除了在循環計算器中
嘗試在代碼中遵循相同的命名約定。例如,若是你有一個特定類型的對象,調用它相同的名稱:
var element = getElement();
不用忽然以爲稱之爲node:
var node = getElement();
若是你遵循與代碼庫中其餘地方相同的命名約定,閱讀代碼的任何人均可以基於此變量在別的地方的命名含義安全地假設它在此處的含義。
未定義不是一個對象!
每一個人的最愛。讓咱們拋開JavaScript的例子,讓咱們確保代碼拋出的任何錯誤都是有意義的消息。
什麼可使錯誤消息有意義?
它應該描述錯誤是什麼
若是可能,它應該包括任何致使錯誤地變量值或其餘數據
關鍵點:拋出的錯誤應該幫助咱們找出哪裏出錯了——所以錯誤消息應該像函數那樣告訴咱們應該怎麼作
自我文檔化代碼的語法相關方法能夠有一些語言特色。例如,Ruby和Perl容許你寫一些奇怪的語法技巧,通常來講,應該避免。
讓咱們來看幾個在JavaScript中遇到的問題:
不要使用語法技巧。這很容易讓人疑惑:
imTricky && doMagic();
上面的這行代碼至關於以下更健全的代碼:
if(imTricky) { doMagic(); }
習慣使用後一種寫法,語法技巧並不討任何人的喜歡。
若是你的代碼中有特殊值——例如數字或字符串值,請考慮使用常量命名。即便如今看起來很清楚,但在一個月或者兩個月後,沒人會知道爲何這麼一個特定的號碼放在那裏,意義是什麼。
const MEANING_OF_LIFE = 42;
(若是你不使用ES6,你能夠用var
,是同樣的。)
布爾值會讓人難以理解代碼,考慮這個:
myThing.setData({ x: 1 }, true);
此處true
的做用是什麼呢?除非找到setDate()
方法並閱讀它。
相反你能夠添加另外一個函數,或重命名現有的函數:
myThing.mergeData({ x: 1 });
如今,你當即就能夠知道這行代碼發生了什麼。
咱們甚至可使用咱們編寫的語言的一些特徵來更好地表述代碼背後的意義。
JavaScript中一個很好的例子是數組的迭代:
var ids = []; for(var i = 0; i < things.length; i++) { ids.push(things[i].id); }
上面的代碼將一個ID列表收集到一個新的數組中,可是爲了理解這塊代碼是作什麼的,咱們須要閱讀整個循環的所有。下面咱們使用map()
來進行比較:
var ids = things.map(function(thing) { return thing.id; });
在這種狀況下,咱們當即知道這會產生一系列的新東西由於這是map()
的目的。若是你有更復雜的循環邏輯,這是頗有益的寫法。list of other iteration functions on MDN
JavaScript的另外一個好例子是const
關鍵字。
一般,你聲明的變量值應該永遠不會改變,一個常見的例子是使用CommonJS加載模塊時:
var async = require('async');
你能夠用以下寫法作出不糊改變意圖的語句:
const async = require('async');
做爲一個額外的好處,若是有人不當心試圖改變這一點,咱們將會獲得一個錯誤。
經過全部這些方法,你能夠作不少事情,可是,有些事情你應該注意。
有些人主張使用簡短的小函數,若是你把全部東西都提取出來,那就是你能獲得的。可是,這可能不利於代碼的理解程度。
例如,假設你正在調試一些代碼。你想查看a()
函數,而後你會發現b()
函數,接着你會發現使用到c()
函數,等等。
雖然簡短的功能能夠很好並且易於理解,但若是你只在一個地方使用該功能,那麼請考慮使用replace expression with variable
方法。
像往常那樣,沒有絕對正確地方法來使代碼自我文檔化。所以,若是某些東西彷佛是一個好主意,但不能強制使用。
使你的代碼自我文檔化能夠大大提升代碼的可維護性,每一個註釋都是須要額外維護的,因此在有可能刪除註釋的狀況下,編寫自我文檔化的代碼是一個好選擇。
可是自我文檔化的代碼並不能取代文檔或者註釋,例如,代碼自己在表達意圖的時候收到限制時,你仍是須要有很好的註釋的。在一些庫中,API文檔是很重要的,所以單純靠閱讀代碼是不可取的,除非你的庫很是小。