“別給糟糕的代碼加注釋——重新寫吧,”—Brian W. Kernighan與P. J. Plaugher
什么也比不上放置良好的注釋來得有用,什么也不會比亂七八糟的注釋更有本事搞亂一個模塊,什么也不會比陳舊、提供錯誤資訊的注釋更有破壞性,
注釋并不像辛德勒的名單,它們并不“純然地好”,實際上,注釋最多也就是一種必須的惡,若編程語言足夠有表達力,或者我們長于用這些語言來表達意圖,就不那么需要注釋——也許根本不需要,
注釋的恰當用法是彌補我們在用代碼表達意圖時遭遇的失敗,注意,我用了“失敗”一詞,我是說真的,注釋總是一種失敗,我們總無法找到不用注釋就能表達自我的方法,所以總要有注釋,這并不值得慶賀,
如果你發現自己需要寫注釋,再想想看是否有辦法翻盤,用代碼來表達,每次用代碼表達,你都該夸獎一下自己,每次寫注釋,你都該做個鬼臉,感受自己在表達能力上的失敗,
我為什么要極力貶低注釋?因為注釋會撒謊,也不是說總是如此或有意如此,但出現得實在太頻繁,注釋存在的時間越久,就離其所描述的代碼越遠,越來越變得全然錯誤,原因很簡單,程式員不能堅持維護注釋,
代碼在變動,在演化,從這里移到那里,彼此分離、重造又合到一處,很不幸,注釋并不總是隨之變動——不能總是跟著走,注釋常常會與其所描述的代碼分隔開來,孑然飄零,越來越不準確,例如,看看以下注釋以及它本來要描述的代碼行變成了什么樣子:
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"
在HTTP_DATE_REGEXP常量及其注釋之間,有可能插入其他物體變數,
程式員應當負責將注釋保持在可維護、有關聯、精確的高度,我同意這種說法,但我更主張把力氣用在寫清楚代碼上,直接保證無須撰寫注釋,
不準確的注釋要比沒注釋壞得多,它們滿口胡言,它們預期的東西永不能實作,它們設定了無需也不應再遵循的舊規則,
真實只在一處地方有:代碼,只有代碼能忠實地告訴你它做的事,那是唯一真正準確的資訊來源,所以,盡管有時也需要注釋,我們也該多花心思盡量減少注釋量,
1 注釋不能美化糟糕的代碼
寫注釋的常見動機之一是糟糕的代碼的存在,我們撰寫一個模塊,發現它令人困擾、亂七八糟,我們知道,它爛透了,我們告訴自己:“喔,最好寫點注釋!”不!最好是把代碼弄干凈!
帶有少量注釋的整潔而有表達力的代碼,要比帶有大量注釋的零碎而復雜的代碼像樣得多,與其花時間撰寫解釋你搞出的糟糕的代碼的注釋,不如花時間清潔那堆糟糕的代碼,
2 用代碼來闡述
有時,代碼本身不足以解釋其行為,不幸的是,許多程式員據此以為代碼很少——如果有的話——能做好解釋作業,這種觀點純屬錯誤,你愿意看到這個:
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) &&
(employee.age > 65))
還是這個?
if (employee.isEligibleForFullBenefits())
只要想上那么幾秒鐘,就能用代碼解釋你大部分的意圖,很多時候,簡單到只需要創建一個描述與注釋所言同一事物的函式即可,
3 好注釋
有些注釋是必須的,也是有利的,來看看一些我認為值得寫的注釋,不過要記住,唯一真正好的注釋是你想辦法不去寫的注釋,
3.1 法律資訊
有時,公司代碼規范要求撰寫與法律有關的注釋,例如,著作權及著作權宣告就是必須和有理由在每個源檔案開頭注釋處放置的內容,
下例是我們在FitNesse專案每個源檔案開頭放置的標準注釋,我可以很開心地說,IDE自動卷起這些注釋,這樣就不會顯得凌亂了,
// 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.
這類注釋不應是合同或法典,只要有可能,就指向一份標準許可或其他外部檔案,而不要把所有條款放到注釋中,
3.2 提供資訊的注釋
有時,用注釋來提供基本資訊也有其用處,例如,以下注釋解釋了某個抽象方法的回傳值:
// Returns an instance of the Responder being tested.
protected abstract Responder responderInstance();
這類注釋有時管用,但更好的方式是盡量利用函式名稱傳達資訊,比如,在本例中,只要把函式重新命名為responderBeingTested,注釋就是多余的了,
下例稍好一些:
// format matched kk:mm:ss EEE, MMM dd, yyyy
Pattern timeMatcher = Pattern.compile(
"\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");
在本例中,注釋說明,該正則運算式意在匹配一個經由SimpleDateFormat.format函式利用特定格式字串格式化的時間和日期,同樣,如果把這段代碼移到某個轉換日期和時間格式的類中,就會更好、更清晰,而注釋也就變得多此一舉了,
3.3 對意圖的解釋
有時,注釋不僅提供了有關實作的有用資訊,而且還提供了某個決定后面的意圖,在下例中,我們看到注釋反映出來的一個有趣決定,在對比兩個物件時,作者決定將他的類放置在比其他東西更高的位置,
public int compareTo(Object o)
{
if(o instanceof WikiPagePath)
{
WikiPagePath p = (WikiPagePath) o;
String compressedName = StringUtil.join(names, "");
String compressedArgumentName = StringUtil.join(p.names, "");
return compressedName.compareTo(compressedArgumentName);
}
return 1; // we are greater because we are the right type.
}
下面的例子甚至更好,你也許不同意程式員給這個問題提供的解決方案,但至少你知道他想干什么,
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());
}
3.4 闡釋
有時,注釋把某些晦澀難明的引數或回傳值的意義翻譯為某種可讀形式,也會是有用的,通常,更好的方法是盡量讓引數或回傳值自身就足夠清楚;但如果引數或回傳值是某個標準庫的一部分,或是你不能修改的代碼,幫助闡釋其含義的代碼就會有用,
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
}
當然,這也會冒闡釋性注釋本身就不正確的風險,回頭看看上例,你會發現想要確認注釋的正確性有多難,這一方面說明了闡釋有多必要,另外也說明了它有風險,所以,在寫這類注釋之前,考慮一下是否還有更好的辦法,然后再加倍小心地確認注釋正確性,
3.5 警示
有時,用于警告其他程式員會出現某種后果的注釋也是有用的,例如,下面的注釋解釋了為什么要關閉某個特定的測驗用例:
// 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);
}
當然,如今我們多數會利用附上恰當解釋性字串的@Ignore屬性來關閉測驗用例,比如@Ignore("Takes too long to run[2]"),但在JUnit4之前的日子里,慣常的做法是在方法名前面加上下劃線,如果注釋足夠有說服力,就會很有用了,
這里有個更麻煩的例子:
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;
}
你也許會抱怨說,還會有更好的解決方法,我大概會同意,不過上面的注釋絕對有道理存在,它能阻止某位急切的程式員以效率之名使用靜態初始器,
3.6 TODO注釋
有時,有理由用//TODO形式在源代碼中放置要做的作業串列,在下例中,TODO注釋解釋了為什么該函式的實作部分無所作為,將來應該是怎樣,
//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;
}
TODO是一種程式員認為應該做,但由于某些原因目前還沒做的作業,它可能是要提醒洗掉某個不必要的特性,或者要求他人注意某個問題,它可能是懇請別人取個好名字,或者提示對依賴于某個計劃事件的修改,無論TODO的目的如何,它都不是在系統中留下糟糕的代碼的借口,
如今,大多數好IDE都提供了特別的手段來定位所有TODO注釋,這些注釋看來丟不了,你不會愿意代碼因為TODO的存在而變成一堆垃圾,所以要定期查看,洗掉不再需要的,
3.7 放大
注釋可以用來放大某種看來不合理之物的重要性,
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()));
3.8 公共API中的Javadoc
沒有什么比被良好描述的公共API更有用和令人滿意的了,標準Java庫中的Javadoc就是一例,沒有它們,寫Java程式就會變得很難,
如果你在撰寫公共API,就該為它撰寫良好的Javadoc,不過要記住本章中的其他建議,就像其他注釋一樣,Javadoc也可能誤導、不適用或者提供錯誤資訊,
4 壞注釋
大多數注釋都屬此類,通常,壞注釋都是糟糕的代碼的支撐或借口,或者對錯誤決策的修正,基本上等于程式員自說自話,
推薦你閱讀《代碼整潔之道》,第4章中有詳細講解,

本書大致可分為3個部分,前幾章介紹撰寫整潔代碼的原則、模式和實踐,這部分有相當多的示例代碼,讀起來頗具挑戰性,讀完這幾章,就為閱讀第2部分做好了準備,如果你就此止步,只能祝你好運啦!
第2部分最需要花工夫,這部分包括幾個復雜性不斷增加的案例研究,每個案例都清理一些代碼——把有問題的代碼轉化為問題少一些的代碼,這部分極為詳細,你的思維要在講解和代碼段之間跳來跳去,你得分析和理解那些代碼,琢磨每次修改的來龍去脈,
你付出的勞動將在第3部分得到回報,這部分只有一章,列出從上述案例研究中得到的啟示和靈感,在遍覽和清理案例中的代碼時,我們把每個操作理由記錄為一種啟示或靈感,我們嘗試去理解自己對閱讀和修改代碼的反應,盡力了解為什么會有這樣的感受、為什么會如此行事,結果得到了一套描述在撰寫、閱讀、清理代碼時思維方式的知識庫,
如果你在閱讀第2部分的案例研究時沒有好好用功,那么這套知識庫對你來說可能所值無幾,在這些案例研究中,每次修改都仔細注明了相關啟示的標號,這些標號用方括號標出,如:[H22],由此你可以看到這些啟示在何種環境下被應用和撰寫,啟示本身不值錢,啟示與案例研究中清理代碼的具體決策之間的關系才有價值,
如果你跳過案例研究部分,只閱讀了第1部分和第3部分,那就不過是又看了一本關于寫出好軟體的“感覺不錯”的書,但如果你肯花時間琢磨那些案例,亦步亦趨——站在作者的角度,迫使自己以作者的思維路徑考慮問題,就能更深刻地理解這些原則、模式、實踐和啟示,這樣的話,就像一個熟練地掌握了騎車的技術后,自行車就如同其身體的延伸部分那樣;對你來說,本書所介紹的整潔代碼的原則、模式、實踐和啟示就成為了本身具有的技藝,而不再是“感覺不錯”的知識,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/345809.html
標籤:其他
