Clean Code第四章:注釋 --閱讀與討論

摘自《Clean Code》Chapter 4

注釋,是一種“必須的惡”。

若編程語言有足夠表達(dá)力,或我們長于用這些語言來表達(dá)意圖,就不那么需要注釋--也許根本不需要。

注釋的恰當(dāng)用法是彌補(bǔ)我們?cè)谟么a表達(dá)意圖時(shí)的失敗。

注意,注釋畢竟是一種失敗,是我們無法找到不用注釋就能表達(dá)的辦法,所以才用注釋。

如果發(fā)現(xiàn)自己需要寫注釋,想想看是否有辦法用代碼來表達(dá)。

每次用代碼表達(dá),都應(yīng)該夸獎(jiǎng)一下自己。每次寫注釋,都應(yīng)該做個(gè)鬼臉,感受一下自己在表達(dá)能力上的失敗。

因?yàn)槌绦騿T不能堅(jiān)持維護(hù)注釋,所以注釋可能會(huì)撒謊。存在的時(shí)間越久,離所描述的代碼越遠(yuǎn)。

例如以下例子,因?yàn)橹虚g插入了代碼,注釋和所要描述的代碼距離很遠(yuǎn):

<pre>
MockRequest request;
private final String HTTP_DATE_REGEXP =
"[SMTWF][a-z]{2},s[0-9]{2}s[JFMASOND][a-z]{2}s"+
"[0-9]{4}s[0-9]{2}:[0-9]{2}:[0-9]{2}sGMT"; private Response response;
private FitNesseContext context;
private FileResponder responder;
private Locale saveLocale;
// Example: "Tue, 02 Apr 2003 22:18:49 GMT"
</pre>

一,注釋不能為了美化糟糕代碼

寫注釋常見的動(dòng)機(jī)是糟糕的代碼的存在。

我們編寫了一個(gè)模塊,發(fā)現(xiàn)它令人困擾亂七八糟,所以就寫了注釋。

而此時(shí)需要做的,反而是要把代碼清理干凈!

帶有少量注釋的整潔代碼,要比有大量注釋的零碎而復(fù)雜的代碼像樣的多。

二,用代碼來闡述

程序員總是傾向于認(rèn)為代碼不足以解釋行為,但其實(shí)只要多想想,就能找到辦法來用代碼闡述行為。

有時(shí)候只是改變函數(shù)名就可以。

例如如下例子:

<pre>
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) && (employee.age > 65))
</pre>

改成:

<pre>
if (employee.isEligibleForFullBenefits())
</pre>

是不是一下子就清晰多了,不需要注釋了。

三,好注釋

有些注釋是必須的和需要寫的。

當(dāng)然,真正好的注釋是你想出辦法不去寫注釋。

1,法律信息

版權(quán)和著作權(quán)聲明是必須和有理由在每個(gè)源文件開頭注釋處放置的內(nèi)容。

<pre>
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.
</pre>

這類注釋不應(yīng)是合同或法典。只要有可能,就指向一份標(biāo)準(zhǔn)許可或其他外部文檔,而不要把所有條款放在注釋中。

2,提供信息的注釋

注釋有時(shí)是為了提供基本信息:

<pre>
// format matched kk:mm:ss EEE, MMM dd, yyyy
Pattern timeMatcher = Pattern.compile("d:d:d* w, w d, d");
</pre>

3,對(duì)意圖的注釋

有時(shí)候,注釋不僅提供了信息,還提供了某個(gè)決定后的意圖:

<pre>
public void testConcurrentAddWidgets() throws Exception {
WidgetBuilder widgetBuilder = new WidgetBuilder(new Class[]{BoldWidget.class});

String text = "'''bold text'''";
ParentWidget parent = new BoldWidget(new MockWidgetRoot(), "'''bold text'''");
AtomicBoolean failFlag = new AtomicBoolean();
failFlag.set(false);
//This is our best attempt to get a race condition
//by creating large number of threads.
for (int i = 0; i < 25000; i++) {
WidgetBuilderThread widgetBuilderThread =
new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
Thread thread = new Thread(widgetBuilderThread);
thread.start();
}
assertEquals(false, failFlag.get());
</pre>

4,闡釋

有時(shí)候,注釋把某些晦澀難明的參數(shù)或返回值翻譯為某種可讀形式。

通常,更好的方法是盡量讓參數(shù)或返回值自身就足夠清楚,但如果參數(shù)或返回值是某個(gè)標(biāo)準(zhǔn)庫的一部分,或者你不能修改的代碼,幫助闡釋其意義就會(huì)有用。

<pre>
public void testCompareTo() throws Exception {
WikiPagePath a = PathParser.parse("PageA");
WikiPagePath ab = PathParser.parse("PageA.PageB");
WikiPagePath b = PathParser.parse("PageB");
WikiPagePath aa = PathParser.parse("PageA.PageA");
WikiPagePath bb = PathParser.parse("PageB.PageB");
WikiPagePath ba = PathParser.parse("PageB.PageA");
assertTrue(a.compareTo(a) == 0); // a == a
assertTrue(a.compareTo(b) != 0); // a != b
assertTrue(ab.compareTo(ab) == 0); // ab == ab
assertTrue(a.compareTo(b) == -1); // a < b
assertTrue(aa.compareTo(ab) == -1); // aa < ab
assertTrue(ba.compareTo(bb) == -1); // ba < bb
assertTrue(b.compareTo(a) == 1); // b > a
assertTrue(ab.compareTo(aa) == 1); // ab > aa
assertTrue(bb.compareTo(ba) == 1);// bb > ba
}
</pre>

5,警示

有時(shí)候,用于警告其他程序員會(huì)出現(xiàn)某種后果的注釋也是有用的。

<pre>
// Don't run unless you
// have some time to kill.
public void _testWithReallyBigFile() {
writeLinesToFile(10000000);
response.setBody(testFile);
response.readyToSend(this);
String responseString = output.toString();
assertSubString("Content-Length: 1000000000", responseString);
assertTrue(bytesSent > 1000000000);
}
</pre>

又例如:

<pre>
public static SimpleDateFormat makeStandardHttpDateFormat() {
//SimpleDateFormat is not thread safe,
//so we need to create each instance independently.
SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");
df.setTimeZone(TimeZone.getTimeZone("GMT"));
return df;
}
</pre>

6,TODO注釋

有時(shí),有理由用//TODO形式在源代碼中放置要做的工作列表。
<pre>
//TODO-MdM these are not needed
// We expect this to go away when we do the checkout model protected VersionInfo makeVersion() throws Exception {
return null;
}
</pre>
7,放大

注釋可以用來放大某種看來不合理的代碼的重要性:
<pre>
String listItemContent = match.group(3).trim();
// the trim is real important. It removes the starting
// spaces that could cause the item to be recognized
// as another list.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
</pre>

8,公共API中的Javdoc

良好描述的公共API是非常令人滿意的。如果在編寫公共API,請(qǐng)為它編寫良好的Javadoc。

四,壞注釋

1, 喃喃自語

有時(shí)候,作者太著急或者沒花心思,結(jié)果注釋成了喃喃自語不知所云。

<pre>
public void loadProperties() {
try {
String propertiesPath = propertiesLocation + "/" + PROPERTIES_FILE;
FileInputStream propertiesStream = new FileInputStream(propertiesPath);
loadedProperties.load(propertiesStream);
}
catch(IOException e) {
// No properties files means all defaults are loaded
}
}
</pre>

2,多余的注釋

有的注釋純屬多余,讀注釋花的時(shí)間可能比讀代碼花的時(shí)間長。

<pre>
// Utility method that returns when this.closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)
throws Exception {
if(!closed) {
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
</pre>

3, 誤導(dǎo)性注釋

有時(shí)候,注釋有誤導(dǎo)性,例如前面出現(xiàn)過的這段:

<pre>
// Utility method that returns when this.closed is true. Throws an exception
// if the timeout is reached.
public synchronized void waitForClose(final long timeoutMillis)
throws Exception {
if(!closed) {
wait(timeoutMillis);
if(!closed)
throw new Exception("MockResponseSender could not be closed");
}
}
</pre>

從注釋里看,在this.closed時(shí)函數(shù)會(huì)立即返回,但實(shí)際上,函數(shù)會(huì)有一段休眠時(shí)間然后再判斷。

如果程序員只看了注釋,則很容易就期望this.closed為真時(shí)立即返回,但代碼中就會(huì)有沒有預(yù)期到的延遲。

4,循規(guī)式注釋

所謂每個(gè)函數(shù)都要有Javadoc或者每個(gè)變量都要有注釋的規(guī)矩是很愚蠢的,徒然讓代碼變得散亂。

<pre>
/** *

  • @param title The title of the CD
  • @param author The author of the CD
  • @param tracks The number of tracks on the CD
  • @param durationInMinutes The duration of the CD in minutes */
    public void addCD(String title, String author,
    int tracks, int durationInMinutes) {
    CD cd = new CD();
    cd.title = title; cd.author = author;
    cd.tracks = tracks;
    cd.duration = duration; cdList.add(cd);
    }
    </pre>

5,日志式注釋

有人會(huì)在每次編輯代碼時(shí),在模塊開始處添加一條注釋。

因?yàn)槲覀儸F(xiàn)在有了源碼控制系統(tǒng)(svn,git等)了,這類冗長的記錄只會(huì)讓模塊變得凌亂,應(yīng)當(dāng)全部刪除。

<pre>

  • Changes (from 11-Oct-2001)

  • 11-Oct-2001 : Re-organised the class and moved it to new package com.jrefinery.date (DG);
  • 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate class (DG);
  • 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate class is gone (DG);
  • Changed getPreviousDayOfWeek(), getFollowingDayOfWeek() and getNearestDayOfWeek() to correct bugs (DG);
  • 05-Dec-2001 :Fixed bug in SpreadsheetDate class (DG);
  • 29-May-2002 :Moved the month constants into a separate interface (MonthConstants) (DG);
  • 27-Aug-2002 :Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
  • 03-Oct-2002 :Fixed errors reported by Checkstyle (DG);
  • 13-Mar-2003 :Implemented Serializable (DG);
  • 29-May-2003 :Fixed bug in addMonths method (DG);
  • 04-Sep-2003 :Implemented Comparable. Updated the isInRange javadocs (DG);
  • 05-Jan-2005 :Fixed bug in addYears() method (1096282) (DG);
    </pre>

6,廢話注釋

有時(shí)候會(huì)看到純?nèi)粫r(shí)廢話的注釋

<pre>
/**

  • Default constructor. /
    protected AnnualDateRule() { }
    </pre>
    <pre>
    /
    * The day of the month. /
    private int dayOfMonth;
    </pre>
    <pre>
    /
    *
  • Returns the day of the month. *
  • @return the day of the month. */
    public int getDayOfMonth() {
    return dayOfMonth;
    }
    </pre>

7,Javadoc廢話

Javadoc也可能是廢話。下列Javadoc來自某知名開源庫,其注釋的目的是什么?

<pre>
/** The name. /
private String name;
/
* The version. /
private String version;
/
* The licenceName. /
private String licenceName;
/
* The version. */
private String info;
</pre>

8,能用函數(shù)或變量時(shí)就別用注釋

例如以下代碼和注釋:

<pre>
// does the module from the global list <mod> depend on the
// subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
</pre>

可以改為:

<pre>
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
</pre>

9,位置標(biāo)記

有時(shí),程序員喜歡在源代碼中標(biāo)記某個(gè)特別位置,但基本上都無用。

<pre>
// Actions //////////////////////////////////
</pre>

10,括號(hào)后面的注釋

有時(shí)程序員會(huì)在深度嵌套結(jié)構(gòu)的函數(shù)中,在括號(hào)后面放置特殊的注釋。

但如果你發(fā)現(xiàn)想標(biāo)記右括號(hào),往往要做的是縮短函數(shù)(見函數(shù)一章)。

<pre>
public class wc {
public static void main(String[] args) {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); String line;
int lineCount = 0;
int charCount = 0;
int wordCount = 0;
try {
while ((line = in.readLine()) != null) {
lineCount++;
charCount += line.length();
String words[] = line.split("W");
wordCount += words.length;
} //while
System.out.println("wordCount = " + wordCount);
System.out.println("lineCount = " + lineCount);
System.out.println("charCount = " + charCount);
} // try
catch (IOException e) {
System.err.println("Error:" + e.getMessage());
} //catch
} //main
}
</pre>

11,歸屬與署名

例如:
<pre>
/* Added by Rick */
</pre>
這是源代碼控制系統(tǒng)做的事情。

如果你寫注釋在那里,隨著一年又一年的過去,注釋就會(huì)越來越不準(zhǔn)確,越來越與原作者沒關(guān)系。

12,注釋掉代碼

把代碼注釋掉,并遺留在那里是很討厭的行為。而我們有了源代碼控制系統(tǒng),一定不要留下這些遺留物。

<pre>
InputStreamResponse response = new InputStreamResponse();
response.setBody(formatter.getResultStream(), formatter.getByteCount());
// InputStream resultsStream = formatter.getResultStream();
// StreamReader reader = new StreamReader(resultsStream);
// response.setContent(reader.read(formatter.getByteCount()));
</pre>

13,注釋中有HTML標(biāo)簽

注釋中的HTML標(biāo)簽會(huì)讓注釋很難讀。

如果需要呈現(xiàn)網(wǎng)頁,那也應(yīng)該由工具生成標(biāo)簽,而不是寫在注釋里。

14,非本地信息

如果一定要寫注釋,請(qǐng)確保它描述了離它最近的代碼,別給出其他地方代碼的信息。

例如以下代碼給出了一個(gè)默認(rèn)端口,而該端口不是由此段代碼所能控制。

如果設(shè)默認(rèn)端口的代碼被修改,此注釋就變成了誤導(dǎo)。

<pre>
/**

  • Port on which fitnesse would run. Defaults to <b>8082</b>. *
  • @param fitnessePort
    */
    public void setFitnessePort(int fitnessePort) {
    this.fitnessePort = fitnessePort;
    }
    </pre>
    15,信息過多

別再注釋中添加有趣的歷史性話題和無關(guān)的細(xì)節(jié)描述,這些只會(huì)增加閱讀代碼的負(fù)擔(dān)。

以下注釋除了RFC文檔編號(hào)外,其他細(xì)節(jié)對(duì)讀者完全沒必要。

<pre>
/*
RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
Part One: Format of Internet Message Bodies
section 6.8. Base64 Content-Transfer-Encoding
The encoding process represents 24-bit groups of input bits as output strings of 4 encoded characters. Proceeding from left to right, a 24-bit input group is formed by concatenating 3 8-bit input groups. These 24 bits are then treated as 4 concatenated 6-bit groups, each of which is translated into a single digit in the base64 alphabet. When encoding a bit stream via the base64 encoding, the bit stream must be presumed to be ordered with the most-significant-bit first. That is, the first bit in the stream will be the high-order bit in the first 8-bit byte, and the eighth bit will be the low-order bit in the first 8-bit byte, and so on.
*/
</pre>
16,不明顯的聯(lián)系

注釋及其描述的代碼之間的聯(lián)系應(yīng)該顯而易見,以下注釋就有些問題:

<pre>
/*

  • start with an array that is big enough to hold all the pixels * (plus filter bytes),
  • and an extra 200 bytes for header info */
    this.pngBytes = new byte[((this.width + 1) * this.height * 3) + 200];

</pre>
以上注釋和代碼里,過濾的字節(jié)指的是什么?與+1還是*3有關(guān)系?為什么用200?

這個(gè)注釋就是失敗的,因注釋本身還需要更多的注釋。

17,函數(shù)頭的注釋

短函數(shù)不需要太多描述。為只做一件事的短函數(shù)取個(gè)好名字,往往比寫函數(shù)頭的注釋要好。

18,非公共代碼中的Javadoc

雖然Javadoc對(duì)于公共API很有用,但對(duì)于不打算作為公共用途的代碼就很沒必要了。對(duì)此類代碼加Javadoc形式的注釋幾乎等同于寫八股文章。

問題:

1,你寫注釋的風(fēng)格是什么樣的?犯了以上哪些錯(cuò)誤?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評(píng)論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,441評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,475評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,834評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,009評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,559評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,306評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,516評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,728評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,249評(píng)論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,484評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容