我應(yīng)該在代碼中寫(xiě)注釋嗎?或者是不惜一切代價(jià)避免呢?抑或是有節(jié)制的使用它們呢?在開(kāi)發(fā)軟件的時(shí)候,所有的開(kāi)發(fā)者對(duì)何時(shí)何地使用注釋都有他們自己的見(jiàn)解。這篇文章只是闡述我的觀點(diǎn),而不是什么真理。
我的這個(gè)觀點(diǎn)僅限于JavaScript面向過(guò)程編程或者面向?qū)ο缶幊獭?/p>
注釋和可維護(hù)性
這篇博客是關(guān)于編寫(xiě)可維護(hù)的代碼,即:
- 容易理解
- 易于擴(kuò)展
- 易于調(diào)試
- 方便測(cè)試
可維護(hù)的代碼就需要大量的注釋嗎?如果代碼寫(xiě)的好,那我覺(jué)得就不是必須的。代碼本身就能說(shuō)明問(wèn)題。
可維護(hù)的代碼不需要任何的注釋嗎?下面我們通過(guò)一些例子來(lái)說(shuō)明。
注釋的演化
方法1:JavaScript diluted with BS
假設(shè)一個(gè)昵稱為 ByTheBook 的初級(jí)開(kāi)發(fā)者下了下面的代碼:
/**
* getWinner
*
* Gets the name of the winner who has the best score from the
* participants array.
*
* @param {Array} participants
* @return {string} the name
* @throws {Error} wrong participant list
*/
function getWinner( participants ) {
var maxScore = 0; // temporary variable to keep maximum score
// initially -1 to always find a participant
var name = null; // name with maximum score
// debugger; // uncomment for debugging
// participants is an array, so it has a length property
for( var i = 0; i < participants.length; ++i ) { // i: loop variable
var currentParticipant = participants[i]; // we examine this participant
if( currentParticipant.score > maxScore ) { // initially any number > null
// we found a new candidate to return
name = currentParticipant.name;
// DON'T FORGET TO UPDATE THE MAX SCORE!!!
maxScore = currentParticipant.score;
}
}
// console.log( name, typeof name );
// Correct result has to be a string
if( typeof name === 'string' ) {
return name;
} else { // technically an else is not needed here
throw new Error( 'Wrong participants' ); // Wrong participants
}
// unreachable
}
這段代碼有什么問(wèn)題嗎?
首先,一個(gè)原則是,不應(yīng)該在你的應(yīng)用中留下任何注釋掉的代碼,這顯得非常的不專業(yè)。如果注釋的代碼只是用于調(diào)試的目的,那將更糟。清理掉你的提交,如果已經(jīng)晚了的話,以后要養(yǎng)成本地提交的習(xí)慣,然后使用 rebase 來(lái)清理掉提交。
保留注釋的代碼只是很大問(wèn)題中的一小部分:從技術(shù)上講,90%的這些注釋的代碼只會(huì)增加麻煩,它們沒(méi)有任何的價(jià)值,而且還會(huì)分散讀者的注意力。例如,在讀完上面的代碼后,你是否考慮到如果有不止一個(gè)的參與者有相同的最高分,我們的程序應(yīng)該如何處理呢。
方法2:Self-documenting code
Flash 是另一個(gè)初級(jí)開(kāi)發(fā)者,假設(shè)他寫(xiě)了下面這段沒(méi)有任何注釋的代碼:
function getWinner( participants ) {
if( participants.length === 0 ) return null;
var lastIndex = participants.length - 1;
var topParticipant = _.sortBy( participants, 'score' )[ lastIndex ];
return topParticipant.name;
}
從技術(shù)上講,該解決方案是有效的。并且,在沒(méi)有參與者的時(shí)候會(huì)返回 null 。然而,該代碼并沒(méi)有對(duì)處理并列第一名的問(wèn)題給出任何的注釋。
另外,在沒(méi)有讀完所有代碼的時(shí)候,你能知道 participants 對(duì)象的格式嗎?你必須從代碼中反向獲得信息:participants 是一個(gè)數(shù)組對(duì)象,它包含 name 和 score 兩個(gè)元素,類型分別為 string 和 integer 。
方法3:The Startup code
Startupper 注意到了該問(wèn)題,然而他有很多其他的事情要做,所以他只是放了一個(gè) TODO 的注釋:
// TODO: sort out ties when two participants have max score
function getWinner( participants ) {
if( participants.length === 0 ) return null;
var lastIndex = participants.length - 1;
var topParticipant = _.sortBy( participants, 'score' )[ lastIndex ];
return topParticipant.name;
}
使用 TODO 的問(wèn)題是它們可能一直保留在代碼中。一些 TODO 可能放在另一些 TODO 前面。一些TODO可能會(huì)與另外的有沖突。另一些可能描述的很不清楚,沒(méi)人能看懂他的初衷是什么。
在這個(gè)例子中 TODO 描述了實(shí)質(zhì)的問(wèn)題,雖然還沒(méi)有給出解決方案。
另外, participants 參數(shù)的類型仍然不清楚。
方法4:Declarative head comments
Declarator 有個(gè)寫(xiě)頭部聲明注釋的習(xí)慣:
- 簡(jiǎn)短而精確的功能描述
- 參數(shù)類型列表以及返回值
- 異常
/**
* getWinner returns the name of the first participant in participants
* with the highest score.
*
* @param {Array of Objects} participants
* Elements: {string} name
* {integer} score
* @return {Mixed(string|null)} name
*/
function getWinner( participants ) {
if( participants.length === 0 ) return null;
var lastIndex = participants.length - 1;
var topParticipant = _.sortBy( participants, 'score' )[ lastIndex ];
return topParticipant.name;
}
該解決方案清楚的記錄了 getWinner 的作用。大部分代碼都非常有幫助,并且做到了與代碼分離,不存在行內(nèi)注釋。
方法5:修復(fù)問(wèn)題
PickyPerfectionist 是一個(gè)不會(huì)妥協(xié)的開(kāi)發(fā)者。他以編寫(xiě)可維護(hù)的代碼為榮。他的口頭禪是:“只有爛代碼才需要注釋”。他的第二個(gè)信條是,從不發(fā)布不夠好的代碼。這意味著他從來(lái)都不寫(xiě)注釋。
在寫(xiě)功能之前,PickyPerfectionist 都會(huì)先明確需求。在有相同最高分的時(shí)候,他會(huì)把所有的參與者姓名都打印出來(lái),以逗號(hào)分隔。
function getWinners( participants ) {
var winnerNames = [];
var winnerScore = null;
for( var i = 0; i < participants.length; ++i ) {
var currentName = participants[i].name;
var currentScore = participants[i].score;
if( currentScore > winnerScore ) {
winnerScore = currentScore;
winnerNames = [ currentName ];
} else if ( currentScore === winnerScore ) {
winnerNames.push( currentName );
}
}
var result = winnerNames.join( ', ' );
return result.length > 0 ? result : null;
}
這段代碼非常易讀。并且解決了前面存在的問(wèn)題,不需要做任何的注釋。雖然對(duì) participants 的結(jié)構(gòu)沒(méi)有給出定義,但你能從代碼中了解到所有你需要的信息。即使函數(shù)名改變了,那也不會(huì)有任何的問(wèn)題。
假設(shè)利益相關(guān)者決定不是列舉所有贏家,他們改變了游戲規(guī)則,只有一個(gè)贏家。這個(gè)你剛才看到過(guò)的注釋非常少的代碼將可以解決問(wèn)題:
function getWinner( participants ) {
if( participants.length === 0 ) return null;
var lastIndex = participants.length - 1;
var topParticipant = _.sortBy( participants, 'score' )[ lastIndex ];
return topParticipant.name;
}
假如有一天,有個(gè)開(kāi)發(fā)者發(fā)現(xiàn)了問(wèn)題,這個(gè)方法只返回第一個(gè)得分最高的參與者?;诎l(fā)現(xiàn)這個(gè)問(wèn)題的開(kāi)發(fā)者類型的不同,有可能會(huì)出現(xiàn)不同的解決方案。解決方案可能是一個(gè) TODO 注釋或者是與相關(guān)利益者要求不符的方法。所有的解決方案都有下面兩個(gè)共同點(diǎn):
- 都會(huì)需要時(shí)間
- 在某種程度上增加麻煩
你會(huì)考慮使用注釋嗎?你有其他避免浪費(fèi)時(shí)間的替代解決方案嗎?如果是的話,假設(shè)你看文檔,這也是需要時(shí)間的。
從技術(shù)上講,自動(dòng)化測(cè)試可以確保只有一個(gè)勝利者。單元測(cè)試可以避免開(kāi)發(fā)者與要求不符的提交。但提前測(cè)試并不能指出問(wèn)題,甚至在特殊情況下,現(xiàn)有的測(cè)試可能會(huì)讓你修改
getWinner方法,而返回多個(gè)贏家。
常識(shí)性的注釋
我的觀點(diǎn)是,只要是有價(jià)值的注釋,都是有用的。沒(méi)有最終的對(duì)與錯(cuò)。
禁止代碼的注釋就是計(jì)算你飲食的卡路里,在一定程度上,它們是有幫助的,如每天攝入2000卡路里而不是8000或者300。吃沙拉的好處遠(yuǎn)比喝汽水要多。相似的,禁止代碼的注釋可以促使你寫(xiě)更好的代碼。然而,在代碼確實(shí)需要注釋的時(shí)候,禁用注釋在長(zhǎng)遠(yuǎn)上看是非常有害的,這最終會(huì)導(dǎo)致代碼混亂,可維護(hù)性差。
什么時(shí)候注釋是有用的呢?首先,JavaScript不允許指定參數(shù)和返回值的類型。因此,關(guān)于類型定義的信息是有用的。這些類型定義在許多其他語(yǔ)言的函數(shù)中都有。如果你知道 == 和 === 的區(qū)別,并且想使用 === ,那么變量類型是很重要的。
比如下面的例子:
/**
* listenTo
*
* Registers callback so that it's immediately called once emitter emits
* the event eventName.
*
* @param {Object} emitter
* @param {string} eventName
* @param {function} callback
*
* @return {void}
*/
listenTo : function( emitter, eventName, callback ) { /* ... */ }
類型定義非常明確。
當(dāng)涉及到類或者函數(shù)定義的時(shí)候,我更偏向于頭部聲明。一句話就可以說(shuō)明類的作用,它里面的方法也一樣。面向過(guò)程的編程,頭部聲明也是很有用的。
頭部注釋聲明也可以包含下面這些:
- 可選參數(shù):有時(shí)候最后一個(gè)參數(shù)可能是未定義的
- 混合類型
- 符合類型:參見(jiàn)方法4
- 異常信息
- 調(diào)用該方法的前提條件
- 副作用
決定在注釋中使用以及避免哪些信息,非常簡(jiǎn)單,卻很重要。一個(gè)周末項(xiàng)目或者原型的注釋與一個(gè)需要長(zhǎng)期開(kāi)發(fā)和維護(hù)的網(wǎng)絡(luò)項(xiàng)目的注釋?xiě)?yīng)該是不同的。
編寫(xiě)優(yōu)秀的頭部注釋是一門(mén)藝術(shù)。但寫(xiě)很好的頭部注釋是需要常識(shí)的。常識(shí)告訴我,頭部注釋?xiě)?yīng)該是包含信息的。下面是一個(gè)不好的例子:
/**
* onRender
*
* Callback on render
*
* @return {void}
*/
onRender : function() {
this.removeAllBindings();
this.cleanOpenedWidgets();
}
而這個(gè)會(huì)好一些:
/**
* onRender: called before rendering the component for the purpose
* of cleaning up model-view bindings and removing nodes left by
* opened widgets.
*
* @return {void}
*/
onRender : function() {
this.removeAllBindings();
this.cleanOpenedWidgets();
}
一個(gè)好的頭部注釋使得99.99%的行內(nèi)注釋變得沒(méi)有意義。我基本上同意以下觀點(diǎn):不管方法多么的復(fù)雜,行內(nèi)注釋只會(huì)分散開(kāi)發(fā)者的注意力,而不會(huì)對(duì)他們有任何幫助。
如果一個(gè)頭部注釋很難寫(xiě),那么這恰恰說(shuō)明你應(yīng)該好好考慮下是否應(yīng)該把方法分解成多個(gè)。因此,頭部注釋可以幫你寫(xiě)出更好的代碼。
從代碼維護(hù)的角度來(lái)看,頭部注釋至少有以下兩個(gè)好處:
- 當(dāng)審核人只需要了解方法的作用的時(shí)候,頭部注釋可以節(jié)省很多時(shí)間;
- 對(duì)于錯(cuò)誤的方法,審核人能夠方便的了解到該方法的原始意圖
為了這些,頭部注釋都是必須的。這并不是一項(xiàng)多么艱難的工作,因?yàn)殚_(kāi)發(fā)者知道他們自己正在做什么。
我是否應(yīng)該寫(xiě)注釋呢?
這取決于你,看哪樣對(duì)你更有幫助。有些項(xiàng)目不需要任何的注釋,而有些則必須包含頭部注釋。不像其他強(qiáng)類型語(yǔ)言,JavaScript的注釋給出了參數(shù)以及返回值的變量類型。緊湊的頭部注釋可以使你的代碼更容易理解。此外,頭部注釋會(huì)使你思考你的代碼,這有時(shí)候會(huì)讓你的代碼更容易維護(hù)。
綜上所述,我不認(rèn)為所有的注釋都是沒(méi)用的。世界也不是絕對(duì)的黑的或者白的。極端的意見(jiàn)通常會(huì)引起更強(qiáng)烈的爭(zhēng)論,而折中的方法在長(zhǎng)遠(yuǎn)來(lái)看卻更加有幫助。
via:zsoltnagy,本文由 Specs 翻譯整理,發(fā)布在 Coder資源網(wǎng),轉(zhuǎn)載請(qǐng)注明來(lái)源。
哈爾濱品用軟件有限公司致力于為哈爾濱的中小企業(yè)制作大氣、美觀的優(yōu)秀網(wǎng)站,并且能夠搭建符合百度排名規(guī)范的網(wǎng)站基底,使您的網(wǎng)站無(wú)需額外費(fèi)用,即可穩(wěn)步提升排名至首頁(yè)。歡迎體驗(yàn)最佳的哈爾濱網(wǎng)站建設(shè)。
