編寫注釋的原因是,在編寫代碼時,編程語言中的語句無法捕獲開發人員頭腦中的所有重要信息。注釋記錄了這些信息,以便以后的開發人員能夠很容易地理解和修改代碼。注釋的指導原則是注釋應該描述代碼中不明顯的內容。
代碼中有許多不明顯的地方。有時是不明顯的低級細節。例如,當一對指標描述一個范圍時,由指標給出的元素是在范圍內還是在范圍外並不明顯。有時不清楚為什么需要代碼,或者為什么以特定的方式實現代碼。有時開發人員會遵循一些規則,比如“總是在b之前調用a”。你可以通過查看所有的代碼來猜測規則,但這是痛苦的,而且容易出錯;注釋可以使規則變得明確和清晰。
注釋最重要的原因之一是抽象,它包含了很多代碼中不明顯的信息。 抽象的思想是提供一種簡單的方法來思考一些事情,但是代碼是如此的詳細,以至於僅僅通過閱讀代碼就很難看到抽象。注釋可以提供更簡單、更高級的視圖(“調用此方法后,網絡流量將被限制為每秒最大帶寬字節”)。即使通過讀取代碼可以推斷出這些信息,我們也不希望強迫模塊的用戶這樣做:讀取代碼非常耗時,並且強迫用戶考慮使用模塊不需要的大量信息。開發人員應該能夠理解模塊提供的抽象,而不需要讀取除其外部可見聲明之外的任何代碼。實現此目的的惟一方法是用注釋補充聲明。
本章討論了在注釋中需要描述哪些信息,以及如何寫出好的注釋。正如您將看到的,好的注釋通常以與代碼不同的細節級別解釋事情,在某些情況下,代碼更詳細,而在其他情況下,代碼更詳細(更抽象)。
13.1 選擇約定
編寫注釋的第一步是決定注釋的慣例,比如注釋的內容和格式。如果您使用的語言存在文檔編譯工具,例如Javadoc (Java)、Doxygen (c++)或godoc (Go) ,請遵循工具的約定。這些約定都不是完美的,但是工具提供了足夠的好處來彌補這一點。如果您在一個沒有現有約定可遵循的環境中進行編程,請嘗試從其他類似的語言或項目中采用這些約定;這將使其他開發人員更容易理解和遵守您的約定。
約定有兩個目的。首先,它們確保一致性,這使得注釋更容易閱讀和理解。其次,它們有助於確保你確實寫了注釋。如果你不清楚你要注釋什么,怎么注釋,很容易最后什么都不寫。
大多數注釋可分為以下幾類:
- 接口:緊接在模塊聲明之前的注釋塊,如類、數據結構、函數或方法。注釋描述了模塊的接口。對於類,注釋描述了類提供的整體抽象。對於一個方法或函數,注釋描述了它的整體行為、參數和返回值(如果有的話)、它產生的任何副作用或異常,以及調用者在調用方法之前必須滿足的任何其他需求。
- 數據結構成員:數據結構中字段聲明旁邊的注釋,例如類的實例變量或靜態變量。
- 實現注釋:方法或函數代碼中的注釋,描述代碼內部的工作方式。
- 跨模塊注釋:描述跨模塊邊界的依賴項的注釋。
最重要的注釋是前兩類。每個類都應該有一個接口注釋,每個類變量都應該有一個注釋,每個方法都應該有一個接口注釋。有時,變量或方法的聲明非常明顯,以至於在注釋中添加任何有用的東西(getter和setter有時屬於此類),但這種情況很少見;與其花精力去擔心是否需要注釋,還不如去注釋所有的事情。執行意見往往是不必要的(見下文第13.6節)。跨模塊注釋是所有注釋中最少見的,編寫它們是有問題的,但是當需要它們時,它們非常重要,第13.7節更詳細地討論了它們。
13.2 不要重復代碼
不幸的是,許多注釋並不是特別有用。最常見的原因是注釋重復了代碼:注釋中的所有信息都可以很容易地從注釋旁邊的代碼推斷出來。 以下是最近一篇研究論文中的代碼示例:
ptr_copy = get_copy(obj) # Get pointer copy
if is_unlocked(ptr_copy): # Is obj free?
return obj # return current obj
if is_copy(ptr_copy): # Already a copy?
return obj # return obj
thread_id = get_thread_id(ptr_copy)
if thread_id == ctx.thread_id: # Locked by current ctx
return ptr_copy # Return copy
除了“Locked by”注釋之外,這些注釋中沒有任何有用的信息,該注釋提示了關於線程的一些信息,而這些信息在代碼中可能並不明顯。請注意,這些注釋的詳細程度與代碼大致相同:每行代碼有一個注釋,用於描述該行。這樣的注釋很少有用。
下面是更多重復代碼的注釋示例:
// Add a horizontal scroll bar
hScrollBar = new JScrollBar(JScrollBar.HORIZONTAL);
add(hScrollBar, BorderLayout.SOUTH);
// Add a vertical scroll bar
vScrollBar = new JScrollBar(JScrollBar.VERTICAL);
add(vScrollBar, BorderLayout.EAST);
// Initialize the caret-position related values
caretX = 0;
caretY = 0;
caretMemX = null;
這些注釋都沒有提供任何價值。對於前兩個注釋,代碼已經足夠清楚,實際上不需要注釋;在第三種情況下,注釋可能是有用的,但是當前的注釋沒有提供足夠的細節來提供幫助。
寫完注釋后,問自己以下問題:從未見過代碼的人能否僅通過查看注釋旁邊的代碼來編寫注釋?如果答案是肯定的,就像上面的例子一樣,那么注釋並不能使代碼更容易理解。像這樣的注釋就是為什么有些人認為這些注釋毫無價值。
另一個常見的錯誤是在注釋中使用與被記錄實體名稱相同的單詞:
/*
* Obtain a normalized resource name from REQ.
*/
private static String[] getNormalizedResourceNames(
HTTPRequest req) ...
/*
* Downcast PARAMETER to TYPE.
*/
private static Object downCastParameter(String parameter, String type) ...
/*
* The horizontal padding of each line in the text.
*/
private static final int textHorizontalPadding = 4;
這些注釋只是從方法名或變量名中提取單詞,或者從參數名和類型中添加一些單詞,並將它們組成一個句子。例如,第二個注釋中唯一不在代碼中的是單詞“to”。同樣,這些注釋可以只通過查看聲明來編寫,而不需要理解變量的方法,因此,它們沒有價值。
危險信號:注釋重復代碼
如果注釋中的信息在注釋旁邊的代碼中已經很明顯,那么注釋就沒有幫助。這方面的一個例子是,注釋使用與它所描述的事物名稱相同的單詞。
同時,注釋中還缺少一些重要的信息:例如,什么是“規范化的資源名稱”,以及getNormalizedResourceNames返回的數組的元素是什么?“沮喪”是什么意思?填充的單位是什么?每行的一邊是填充還是兩邊都是?在注釋中描述這些事情會很有幫助。
編寫好的注釋的第一步是在注釋中使用不同於被描述的實體名稱中的單詞。 為注釋選擇提供關於實體意義的附加信息的單詞,而不是重復它的名字。例如,下面是對textHorizontalPadding的一個更好的注釋:
/*
* The amount of blank space to leave on the left and
* right sides of each line of text, in pixels.
*/
private static final int textHorizontalPadding = 4;
該注釋提供了聲明本身不明顯的附加信息,比如單位(像素)和每行兩邊都有填充。這篇注釋沒有使用“padding”這個詞,而是解釋了padding是什么,以防讀者不熟悉這個詞。
13.3 低級注釋增加了精確性
既然您已經知道了什么是不應該做的,那么讓我們來討論一下您應該在注釋中添加哪些信息。注釋通過提供不同詳細級別的信息來補充代碼。有些注釋提供了比代碼更低、更詳細的信息;這些注釋通過闡明代碼的確切含義來增加精確性。其他注釋提供了比代碼更高、更抽象的信息;這些注釋提供了直覺,比如代碼背后的推理,或者一種更簡單、更抽象的代碼思考方式。與代碼處於同一級別的注釋可能重復代碼。本節將更詳細地討論低級方法,下一節將討論高級方法。
在注釋變量聲明(如類實例變量、方法參數和返回值)時,精度是最有用的。變量聲明中的名稱和類型通常不是很精確。意見可以填補遺漏的細節,如:
- 這個變量的單位是什么?
- 邊界條件是包含的還是排斥的?
- 如果允許空值,它意味着什么?
- 如果一個變量引用了一個最終必須釋放或關閉的資源,那么誰來負責釋放或關閉它呢?
- 對於變量(不變量),是否存在某些始終為真的屬性,例如“此列表始終包含至少一個條目”?
通過檢查變量所使用的所有代碼,可以找出其中的一些信息。然而,這樣做既耗時又容易出錯;聲明的注釋應該足夠清晰和完整,使其沒有必要這樣做。當我說聲明的注釋應該描述代碼中不明顯的內容時,“代碼”指的是注釋(聲明)旁邊的代碼,而不是“應用程序中的所有代碼”。
對於變量的注釋最常見的問題是注釋太模糊。以下是兩個不夠精確的注釋:
/ Current offset in resp Buffer
uint32_t offset;
// Contains all line-widths inside the document and
// number of appearances.
private TreeMap<Integer, Integer> lineWidths;
在第一個例子中,不清楚“current”是什么意思。在第二個示例中,並不清楚TreeMap中的鍵是否為行寬,值是否為出現次數。還有,寬度是用像素還是字符來測量的?下列經修訂的意見提供了更多詳情:
// Position in this buffer of the first object that hasn't
// been returned to the client.
uint32_t offset;
// Holds statistics about line lengths of the form <length, count>
// where length is the number of characters in a line (including
// the newline), and count is the number of lines with
// exactly that many characters. If there are no lines with
// a particular length, then there is no entry for that length.
private TreeMap<Integer, Integer> numLinesWithLength;
二個聲明使用了一個更長的名稱,它傳遞了更多的信息。它還將“寬度”改為“長度”,因為這個詞更容易讓人認為單位是字符而不是像素。請注意,第二個注釋不僅記錄了每個條目的詳細信息,還記錄了如果某個條目丟失了,它意味着什么。
記錄變量時,考慮的是名詞,而不是動詞。換句話說,關注變量所表示的內容,而不是它是如何操作的。考慮一下下面的注釋:
/* FOLLOWER VARIABLE: indicator variable that allows the Receiver and the
* PeriodicTasks thread to communicate about whether a heartbeat has been
* received within the follower's election timeout window.
* Toggled to TRUE when a valid heartbeat is received.
* Toggled to FALSE when the election timeout window is reset. */
private boolean receivedValidHeartbeat;
文檔描述了變量是如何被類中的幾段代碼修改的。如果注釋描述了變量所代表的內容,而不是鏡像代碼結構,那么注釋將更短,也更有用:
/* True means that a heartbeat has been received since the last time
* the election timer was reset. Used for communication between the
* Receiver and PeriodicTasks threads. */
private boolean receivedValidHeartbeat;
有了這個文檔,很容易推斷出變量在接收到心跳時必須設置為true,在重置選舉計時器時必須設置為false。
13.4 更高層次的注釋增強直覺
注釋增加代碼的第二種方式是提供直覺。這些注釋是在比代碼更高的級別上編寫的。它們省略了細節,幫助讀者理解代碼的總體意圖和結構。這種方法通常用於方法內部的注釋和接口注釋。例如,考慮以下代碼:
// If there is a LOADING readRpc using the same session
// as PKHash pointed to by assignPos, and the last PKHash
// in that readRPC is smaller than current assigning
// PKHash, then we put assigning PKHash into that readRPC.
int readActiveRpcId = RPC_ID_NOT_ASSIGNED;
for (int i = 0; i < NUM_READ_RPC; i++) {
if (session == readRpc[i].session
&& readRpc[i].status == LOADING
&& readRpc[i].maxPos < assignPos
&& readRpc[i].numHashes < MAX_PKHASHES_PERRPC) {
readActiveRpcId = i;
break;
}
}
這個注釋太低級,太詳細了。一方面,它部分地重復代碼:“如果有加載readRPC”只是重復了測試readRPC [i]。= =加載狀態。另一方面,注釋沒有解釋這段代碼的總體目的,也沒有解釋它如何適合包含它的方法。因此,注釋並不能幫助讀者理解代碼。
下面是一個更好的注釋:
// Try to append the current key hash onto an existing
// RPC to the desired server that hasn't been sent yet.
這條注釋沒有包含任何細節。相反,它在更高的層次上描述代碼的整體功能。有了這些高級信息,讀者幾乎可以解釋代碼中發生的所有事情:循環必須遍歷所有現有的遠程過程調用(rpc);會話測試可能用於查看某個特定RPC是否指向正確的服務器;加載試驗表明,rpc可以有多種狀態,在某些狀態下添加更多的散列是不安全的;MAX - PKHASHES_PERRPC測試表明,在一個RPC中可以發送多少個散列是有限制的。注釋中唯一沒有解釋的是maxPos測試。此外,新的注釋為讀者判斷代碼提供了一個基礎:它是否完成了向現有RPC添加鍵散列所需的所有工作?最初的注釋並沒有描述代碼的總體意圖,因此讀者很難判斷代碼的行為是否正確。
高級注釋比低級注釋更難編寫,因為必須以不同的方式考慮代碼。問問你自己:這段代碼要做什么?你能說的最簡單的解釋代碼中的一切的事情是什么?這段代碼最重要的是什么?
工程師往往非常注重細節。我們喜歡細節,擅長管理大量細節;這是成為一名優秀工程師的必要條件。但是,優秀的軟件設計師也可以從細節上退一步,從更高的層次來考慮系統。 這意味着確定系統的哪些方面是最重要的,並且能夠忽略底層的細節,只從系統最基本的特征來考慮系統。這就是抽象的本質(找到一種簡單的方法來考慮復雜的實體),這也是編寫高級注釋時必須做的事情。好的高級注釋表達了一個或幾個提供概念框架的簡單思想,例如“附加到現有RPC”。有了這個框架,就很容易看出特定的代碼語句與總體目標之間的關系。
下面是另一個代碼示例,它有一個很好的高級注釋:
if (numProcessedPKHashes < readRpc[i].numHashes) {
// Some of the key hashes couldn't be looked up in
// this request (either because they aren't stored
// on the server, the server crashed, or there
// wasn't enough space in the response message).
// Mark the unprocessed hashes so they will get
// reassigned to new RPCs.
for (size_t p = removePos; p < insertPos; p++) {
if (activeRpcId[p] == i) {
if (numProcessedPKHashes > 0) {
numProcessedPKHashes--;
} else {
if (p < assignPos)
assignPos = p;
activeRpcId[p] = RPC_ID_NOT_ASSIGNED;
}
}
}
}
這個注釋做了兩件事,第二句提供了代碼功能的抽象描述。第一句話是不同的:它解釋了(用高級術語)為什么執行代碼。表單“我們如何到達這里”的注釋對於幫助人們理解代碼非常有用。例如,在記錄方法時,描述最有可能調用該方法的條件(特別是如果該方法僅在不尋常的情況下調用)可能非常有用。
13.5 接口文檔
注釋最重要的角色之一是定義抽象。 回顧第4章,抽象是一個實體的簡化視圖,它保留了基本信息,但是忽略了一些可以忽略的細節。代碼不適合描述抽象;它的層次太低,並且包含了在抽象中不應該出現的實現細節。描述抽象的唯一方法是使用注釋。如果您希望代碼呈現良好的抽象,則必須用注釋記錄這些抽象。
記錄抽象的第一步是將接口注釋從實現注釋中分離出來。接口注釋提供了為了使用類或方法而需要知道的信息;他們定義了抽象。實現注釋描述類或方法如何在內部工作以實現抽象。將這兩種注釋分開很重要,這樣界面的用戶就不會暴露於實現細節。此外,這兩種形式最好是不同的。如果接口注釋也必須描述實現,那么類或方法是淺層的。這意味着寫注釋的行為可以提供關於設計質量的線索;第15章將回到這個觀點。
類的接口注釋提供了類提供的抽象的高級描述,例如:
/**
* This class implements a simple server-side interface to the HTTP
* protocol: by using this class, an application can receive HTTP
* requests, process them, and return responses. Each instance of
* this class corresponds to a particular socket used to receive
* requests. The current implementation is single-threaded and
* processes one request at a time.
*/
public class Http {...}
這個注釋描述了類的整體功能,沒有任何實現細節,甚至沒有特定方法的細節。它還描述了類的每個實例所代表的內容。最后,注釋描述了該類的局限性(它不支持來自多個線程的並發訪問),這對於正在考慮是否使用該類的開發人員可能很重要。
方法的接口注釋包括用於抽象的高級信息和用於精確的低級細節:
- 注釋通常以一兩句話開頭,描述調用者所感知到的方法行為,這是更高層次的抽象。
- 注釋必須描述每個參數和返回值(如果有的話)。這些注釋必須非常精確,並且必須描述關於參數值的任何約束以及參數之間的依賴關系。
- 如果該方法有任何副作用,則必須在接口注釋中記錄這些副作用。副作用是影響系統未來行為的任何方法的結果,但不是結果的一部分。例如,如果該方法向內部數據結構添加一個值,該值可以通過將來的方法調用來檢索,這是一個副作用;寫入文件系統也是一個副作用。
- 方法的接口注釋必須描述從該方法產生的任何異常。
- 如果在調用方法之前必須滿足任何先決條件,則必須對這些條件進行描述(可能必須先調用其他方法,對於二叉搜索方法,要搜索的列表必須排序)。將先決條件最小化是個好主意,但是任何保留的條件都必須記錄下來。
下面是一個方法的接口注釋,該方法復制緩沖區對象中的數據:
/**
* Copy a range of bytes from a buffer to an external location.
*
* \param offset
* Index within the buffer of the first byte to copy.
* \param length
* Number of bytes to copy.
* \param dest
* Where to copy the bytes: must have room for at least
* length bytes.
*
* \return
* The return value is the actual number of bytes copied,
* which may be less than length if the requested range of
* bytes extends past the end of the buffer. 0 is returned
* if there is no overlap between the requested range and
* the actual buffer.
*/
uint32_t
Buffer::copy(uint32_t offset, uint32_t length, void* dest)
...
此注釋的語法(例如,\return)遵循Doxygen的慣例,Doxygen是一個從C/ c++代碼中提取注釋並將其編譯成Web頁面的程序。注釋的目的是提供開發人員調用該方法所需的所有信息,包括如何處理特殊情況(請注意該方法如何遵循第10章的建議並定義與范圍規范相關的錯誤)。開發人員不需要讀取方法的主體來調用它,而且接口注釋沒有提供關於如何實現方法的信息,例如如何掃描內部數據結構來找到所需的數據。
對於一個更擴展的示例,讓我們考慮一個名為IndexLookup的類,它是分布式存儲系統的一部分。存儲系統持有一組表,每個表包含許多對象。此外,每個表可以有一個或多個索引;每個索引根據對象的特定字段提供對表中對象的有效訪問。例如,一個索引可能用於根據對象的名稱字段查找對象,另一個索引可能用於根據對象的年齡字段查找對象。使用這些索引,應用程序可以快速提取具有特定名稱的所有對象,或具有給定范圍內年齡的所有對象。
IndexLookup類為執行索引查找提供了一個方便的接口。下面是一個如何在應用程序中使用它的例子:
query = new IndexLookup(table, index, key1, key2);
while (true) {
object = query.getNext();
if (object == NULL) {
break;
}
... process object ...
}
應用程序首先構造一個IndexLookup類型的對象,提供參數,選擇一個表,索引,一個范圍內的索引(例如,如果指數是基於一個年齡字段,key1和key2可能指定為21和65年選擇所有對象與年齡之間的值)。然后應用程序重復調用getNext方法。每次調用返回一個落在期望范圍內的對象;一旦所有匹配的對象都被返回,getNext返回NULL。因為存儲系統是分布式的,這個類的實現有點復雜。一個表中的對象可以分布在多個服務器上,每個索引也可以分布在不同的一組服務器上;IndexLookup類中的代碼必須首先與所有相關的索引服務器通信,以收集關於范圍內對象的信息,然后必須與實際存儲對象的服務器通信,以便檢索它們的值。
現在,讓我們考慮需要在這個類的接口注釋中包含哪些信息。對於下面給出的每一條信息,問問自己開發人員是否需要知道這些信息才能使用這個類(我的答案在本章的最后):
- IndexLookup類發送給保存索引和對象的服務器的消息格式。
- 用於確定特定對象是否在期望范圍內的比較函數(是否使用整數、浮點數或字符串進行比較?)
- 用於在服務器上存儲索引的數據結構。
- IndexLookup是否同時向不同的服務器發出多個請求。
- 處理服務器崩潰的機制。
這是IndexLookup類的接口注釋的原始版本;這段摘錄還包括了類定義中的幾行,它們在注釋中被引用:
*
* This class implements the client side framework for index range
* lookups. It manages a single LookupIndexKeys RPC and multiple
* IndexedRead RPCs. Client side just includes "IndexLookup.h" in
* its header to use IndexLookup class. Several parameters can be set
* in the config below:
* - The number of concurrent indexedRead RPCs
* - The max number of PKHashes a indexedRead RPC can hold at a time
* - The size of the active PKHashes
*
* To use IndexLookup, the client creates an object of this class by
* providing all necessary information. After construction of
* IndexLookup, client can call getNext() function to move to next
* available object. If getNext() returns NULL, it means we reached
* the last object. Client can use getKey, getKeyLength, getValue,
* and getValueLength to get object data of current object.
*/
class IndexLookup {
...
private:
/// Max number of concurrent indexedRead RPCs
static const uint8_t NUM_READ_RPC = 10;
/// Max number of PKHashes that can be sent in one
/// indexedRead RPC
static const uint32_t MAX_PKHASHES_PERRPC = 256;
/// Max number of PKHashes that activeHashes can
/// hold at once.
static const size_t MAX_NUM_PK = (1 << LG_BUFFER_SIZE);
}
在進一步閱讀之前,看看您是否能夠識別這條注釋的問題。以下是我發現的問題:
- 第一段的大部分內容涉及實現,而不是接口。例如,用戶不需要知道用於與服務器通信的特定遠程過程調用的名稱。第一段后半部分提到的配置參數都是私有變量,它們只與類的維護者相關,而與類的用戶無關。所有這些實現信息都應該從注釋中刪除。
- 這條注釋還包括幾件顯而易見的事情。例如,不需要告訴用戶包含IndexLookup。h:任何編寫c++代碼的人都能猜到這是必要的。此外,文本“通過提供所有必要的信息”沒有說什么,所以可以省略。
這個類的簡短注釋就足夠了(更可取):
* This class is used by client applications to make range queries
* using indexes. Each instance represents a single range query.
*
* To start a range query, a client creates an instance of this
* class. The client can then call getNext() to retrieve the objects
* in the desired range. For each object returned by getNext(), the
* caller can invoke getKey(), getKeyLength(), getValue(), and
* getValueLength() to get information about that object.
*/
這個注釋的最后一段並不是嚴格必需的,因為它主要是重復了各個方法注釋中的信息。但是,在類文檔中提供一些示例來說明它的方法如何協同工作是很有幫助的,特別是對於使用模式不明顯的深度類。注意,新注釋沒有提到來自getNext的NULL返回值。此注釋並不打算記錄每個方法的每個細節;它只是提供高級信息來幫助讀者理解這些方法如何協同工作,以及何時可以調用每個方法。有關詳細信息,讀者可以參考各個方法的接口注釋。這條注釋也沒有提到服務器崩潰;這是因為服務器崩潰對於此類用戶是不可見的(系統會自動從中恢復)。
嚴重警告:實現文檔會污染接口
當接口文檔(例如用於方法的文檔)描述了使用所記錄的內容不需要的實現細節時,就會出現此警告。
現在考慮下面的代碼,它顯示了IndexLookup中isReady方法的第一個文檔版本:
/**
* Check if the next object is RESULT_READY. This function is
* implemented in a DCFT module, each execution of isReady() tries
* to make small progress, and getNext() invokes isReady() in a
* while loop, until isReady() returns true.
*
* isReady() is implemented in a rule-based approach. We check
* different rules by following a particular order, and perform
* certain actions if some rule is satisfied.
*
* \return
* True means the next Object is available. Otherwise, return
* false.
*/
bool IndexLookup::isReady() { ... }
同樣,大多數文檔,例如對DCFT的引用和整個第二段,都涉及到實現,所以它不屬於這里;這是接口注釋中最常見的錯誤之一。一些實現文檔是有用的,但是它應該放在方法內部,在那里它將與接口文檔清晰地分離。此外,文檔的第一句話含義模糊(RESULT_READY是什么意思?),而且缺少一些重要的信息。最后,這里沒有必要描述getNext的實現。下面是這條注釋的更好版本:
*
* Indicates whether an indexed read has made enough progress for
* getNext to return immediately without blocking. In addition, this
* method does most of the real work for indexed reads, so it must
* be invoked (either directly, or indirectly by calling getNext) in
* order for the indexed read to make progress.
*
* \return
* True means that the next invocation of getNext will not block
* (at least one object is available to return, or the end of the
* lookup has been reached); false means getNext may block.
*/
這個版本的注釋提供了關於“ready”含義的更精確的信息,並且提供了重要的信息,如果要繼續進行索引檢索,最終必須調用這個方法。
13.6 建議:什么和為什么,而不是如何
注釋是出現在方法內部以幫助讀者理解其內部工作方式的注釋。大多數方法都很簡短,不需要任何實現注釋:只要有代碼和接口注釋,就很容易弄清楚方法是如何工作的。
注釋的主要目標是幫助讀者理解代碼在做什么(而不是如何做)。 一旦讀者知道代碼要做什么,通常就很容易理解代碼是如何工作的。對於簡短的方法,代碼只做一件事,這已經在接口注釋中描述過了,所以不需要實現注釋。較長的方法有幾個代碼塊,它們作為方法整體任務的一部分,執行不同的任務。在每個主要塊之前添加注釋,以提供該塊功能的高級(更抽象)描述。下面是一個例子:
// Phase 1: Scan active RPCs to see if any have completed.
對於循環,在循環之前有一個注釋是很有幫助的,它描述了每次迭代中發生的事情:
// Each iteration of the following loop extracts one request from
// the request message, increments the corresponding object, and
// appends a response to the response message.
注意這個注釋是如何在更抽象和直觀的層次上描述循環的;它不涉及如何從請求消息中提取請求或如何增加對象的任何細節。循環注釋只在較長或更復雜的循環中需要,在這種情況下,可能不清楚循環在做什么;許多循環都很短很簡單,它們的行為已經很明顯了。
了描述代碼在做什么之外,實現注釋還有助於解釋其原因。如果代碼中有一些難以處理的方面,通過閱讀不會很明顯,那么您應該將它們記錄下來。例如,如果一個bug修復需要添加一些目的不太明顯的代碼,那么可以添加一條注釋,說明為什么需要這些代碼。對於編寫良好的bug報告描述問題的bug修復,注釋可以參考bug跟蹤數據庫中的問題,而不是重復它的所有細節(“修復RAM-436,與Linux 2.4.x中的設備驅動程序崩潰有關”)。開發人員可以在bug數據庫中查找更多細節(這是避免注釋重復的一個例子,將在第16章中討論)。
對於較長的方法,為一些最重要的局部變量寫注釋是有幫助的。但是,大多數局部變量如果名稱良好,則不需要文檔。如果一個變量的所有用法都可以在彼此的幾行代碼中看到,那么無需注釋就可以很容易地理解這個變量的用途。在這種情況下,可以讓讀者閱讀代碼來理解變量的含義。但是,如果變量在大范圍的代碼中使用,那么應該考慮添加注釋來描述變量。在記錄變量時,要關注變量表示什么,而不是如何在代碼中操作變量。
13.7 跨模塊設計決策
在一個完美的世界中,每個重要的設計決策都被封裝在一個類中。不幸的是,實際系統不可避免地以影響多個類的設計決策而告終。例如,網絡協議的設計將同時影響發送方和接收方,這些可能在不同的地方實現。跨模塊決策通常是復雜而微妙的,它們會導致許多bug,因此為它們編寫良好的文檔是至關重要的。
跨模塊文檔的最大挑戰是找到一個地方將其放置在開發人員自然會發現的地方。有時,放置這樣的文檔有一個明顯的中心位置。例如,RAMCloud存儲系統定義了一個狀態值,該值由每個請求返回,以指示成功或失敗。為新的錯誤條件添加狀態需要修改許多不同的文件(一個文件將狀態值映射到異常,另一個文件為每個狀態提供人類可讀的消息,等等)。幸運的是,當添加一個新的狀態值時,有一個地方是開發人員必須去的,那就是狀態enum的聲明。我們利用了這一點,在enum中添加了注釋,以確定所有其他必須修改的地方:
typedef enum Status {
STATUS_OK = 0,
STATUS_UNKNOWN_TABLET = 1,
STATUS_WRONG_VERSION = 2,
...
STATUS_INDEX_DOESNT_EXIST = 29,
STATUS_INVALID_PARAMETER = 30,
STATUS_MAX_VALUE = 30,
// Note: if you add a new status value you must make the following
// additional updates:
// (1) Modify STATUS_MAX_VALUE to have a value equal to the
// largest defined status value, and make sure its definition
// is the last one in the list. STATUS_MAX_VALUE is used
// primarily for testing.
// (2) Add new entries in the tables "messages" and "symbols" in
// Status.cc.
// (3) Add a new exception class to ClientException.h
// (4) Add a new "case" to ClientException::throwException to map
// from the status value to a status-specific ClientException
// subclass.
// (5) In the Java bindings, add a static class for the exception
// to ClientException.java
// (6) Add a case for the status of the exception to throw the
// exception in ClientException.java
// (7) Add the exception to the Status enum in Status.java, making
// sure the status is in the correct position corresponding to
// its status code.
}
新的狀態值將被添加到現有列表的末尾,因此注釋也被放置在最可能看到它們的末尾。
不幸的是,在許多情況下,沒有一個明顯的中心位置來放置跨模塊文檔。RAMCloud存儲系統中的一個例子是處理僵屍服務器的代碼,僵屍服務器是系統認為已經崩潰但實際上仍在運行的服務器。中和zombie server需要幾個不同模塊中的代碼,這些代碼都相互依賴。沒有一段代碼明顯是放置文檔的中心位置。一種可能性是在每個依賴文檔的位置復制文檔的部分。然而,這是令人尷尬的,並且隨着系統的發展,很難使這樣的文檔保持最新。或者,文檔可以位於需要它的位置之一,但是在這種情況下,開發人員不太可能看到文檔或者知道在哪里查找它。
我最近試驗了一種方法,將跨模塊問題記錄在一個名為designNotes的中心文件中。ile被划分為有明確標記的部分,每個主要主題對應一個部分。例如,以下是該文件的摘錄:
...
Zombies
-------
A zombie is a server that is considered dead by the rest of the
cluster; any data stored on the server has been recovered and will
be managed by other servers. However, if a zombie is not actually
dead (e.g., it was just disconnected from the other servers for a
while) two forms of inconsistency can arise:
* A zombie server must not serve read requests once replacement servers have taken over; otherwise it may return stale data that does not reflect writes accepted by the replacement servers.
* The zombie server must not accept write requests once replacement servers have begun replaying its log during recovery; if it does, these writes may be lost (the new values may not be stored on the replacement servers and thus will not be returned by reads).
RAMCloud uses two techniques to neutralize zombies. First,
...
在任何與這些問題相關的代碼中,都會有一個關於designNotes文件的簡短注釋:
// See "Zombies" in designNotes.
用這種方法,文檔只有一個副本,開發人員在需要時很容易找到它。然而,這有一個缺點,即文檔不接近任何依賴於它的代碼片段,因此隨着系統的發展可能很難保持最新。
13.8 結論
注釋的目標是確保系統的結構和行為對讀者來說是顯而易見的,這樣他們就可以快速地找到他們需要的信息,並有信心地對系統進行修改。有些信息可以在代碼中以一種讀者已經很容易理解的方式表示,但是有大量的信息不容易從代碼中推斷出來。注釋填寫此信息。
當遵循注釋應該描述代碼中不明顯的東西的規則時,“明顯”是從第一次閱讀您的代碼的人(而不是您)的角度來看的。在寫注釋的時候,試着把自己放在讀者的心態中,問問自己他或她需要知道的關鍵事情是什么。如果你的代碼正在被審查,而審查者告訴你有些東西不明顯,不要和他們爭論;如果讀者認為它不明顯,那么它就不明顯。與其爭論,不如試着理解他們感到困惑的地方,然后看看你是否可以用更好的注釋或更好的代碼來澄清它。
13.9 對第13.5節問題的回答
為了使用IndexLookup類,開發人員是否需要了解以下每個信息片段:
- IndexLookup類發送給保存索引和對象的服務器的消息格式。這是一個實現細節,應該隱藏在類中。
- 用於確定特定對象是否在期望范圍內的比較函數(是否使用整數、浮點數或字符串進行比較?)是的:類的用戶需要知道這些信息。
- 用於在服務器上存儲索引的數據結構。不:這些信息應該封裝在服務器上;甚至IndexLookup的實現也不需要知道這一點。
- IndexLookup是否同時向不同的服務器發出多個請求。可::如果IndexLookup使用特殊技術來提高性能,那么文檔應該提供一些關於這方面的高級信息,因為用戶可能關心性能。
- 處理服務器崩潰的機制。RAMCloud從服務器崩潰中自動恢復,因此應用程序級軟件看不到崩潰;因此,在IndexLookup的接口文檔中不需要提到崩潰。如果崩潰反映到應用程序中,那么接口文檔需要描述它們如何顯示自己(而不是崩潰恢復工作的細節)。