軟體設計為易于閱讀,而不是易于編寫。設計和代碼的可閱讀性是好的設計和編碼一個名額,面向閱讀和維護程式設計。
目錄
- 18.1 使代碼更簡單的東西
- 18.2 使代碼不那麼明顯的事情
- 18.3 結論
晦澀是2.3節中描述的複雜性的兩個主要原因之一。當系統的重要資訊對新開發人員來說不明顯時,就會出現模糊現象。模糊問題的解決方案是用一種簡單易解的方式來寫代碼。本章讨論了一些使代碼或多或少變得簡單的因素。
如果代碼是簡單易解的,這意味着某人可以快速地閱讀代碼,而不需要太多思考,并且他們對代碼的行為或含義的第一次猜測将是正确的。如果代碼是簡單易解的,那麼讀者就不需要花費太多時間或精力來收集處理代碼所需的所有資訊。如果代碼不是簡單易解的,那麼讀者必須花費大量的時間和精力來了解它。這不僅降低了它們的效率,而且還增加了誤解和錯誤的可能性。明顯的代碼比不明顯的代碼需要更少的注釋。
“簡單易解”是讀者的想法:注意到别人的代碼不簡單易解比看到自己代碼的問題更容易。是以,确定代碼可見性的最佳方法是通過代碼審查。如果有人讀了你的代碼,說它不明顯,那麼它就不明顯,不管它在你看來多麼清楚。通過嘗試了解是什麼使代碼變得不明顯,您将了解如何在将來編寫更好的代碼。
在前幾章中已經讨論了使代碼簡單易解的兩個最重要的技術。第一個是選擇好名字 (第14章)。精确而有意義的名稱澄清了代碼的行為,減少了對文檔的需要。如果名稱含糊不清,那麼讀者就會通讀代碼以推斷出指定實體的含義;這既耗時又容易出錯。第二個技巧是 一緻性 (第17章)。如果相似的事情總是以相似的方式進行,那麼讀者可以識别出他們以前見過的模式,并立即得出(安全的)結論,而無需詳細分析代碼。
這裡有一些其他的通用技術,使代碼更簡單易解:
明智地使用空白。 代碼的格式化方式會影響代碼的容易了解程度。考慮以下參數文檔,其中空格已被擠出:
/**
* ...
* @param numThreads The number of threads that this manager should
* spin up in order to manage ongoing connections. The MessageManager
* spins up at least one thread for every open connection, so this
* should be at least equal to the number of connections you expect
* to be open at once. This should be a multiple of that number if
* you expect to send a lot of messages in a short amount of time.
* @param handler Used as a callback in order to handle incoming
* messages on this MessageManager's open connections. See
* {@code MessageHandler} and {@code handleMessage} for details.
*/
很難看到一個參數的文檔在哪裡結束,下一個參數又在哪裡開始。甚至不清楚有多少參數,或者它們的名稱是什麼。如果添加一些空白,結構會突然變得清晰,文檔也更容易掃描:
/**
* @param numThreads
* The number of threads that this manager should spin up in
* order to manage ongoing connections. The MessageManager spins
* up at least one thread for every open connection, so this
* should be at least equal to the number of connections you
* expect to be open at once. This should be a multiple of that
* number if you expect to send a lot of messages in a short
* amount of time.
* @param handler
* Used as a callback in order to handle incoming messages on
* this MessageManager's open connections. See
* {@code MessageHandler} and {@code handleMessage} for details.
*/
空行對于分離方法中的主要代碼塊也很有用,如下例所示:
void* Buffer::allocAux(size_t numBytes)
{
// Round up the length to a multiple of 8 bytes, to ensure alignment.
uint32_t numBytes32 = (downCast<uint32_t>(numBytes) + 7) & ~0x7;
assert(numBytes32 != 0);
// If there is enough memory at firstAvailable, use that. Work down
// from the top, because this memory is guaranteed to be aligned
// (memory at the bottom may have been used for variable-size chunks).
if (availableLength >= numBytes32) {
availableLength -= numBytes32;
return firstAvailable + availableLength;
}
// Next, see if there is extra space at the end of the last chunk.
if (extraAppendBytes >= numBytes32) {
extraAppendBytes -= numBytes32;
return lastChunk->data + lastChunk->length + extraAppendBytes;
}
// Must create a new space allocation; allocate space within it.
uint32_t allocatedLength;
firstAvailable = getNewAllocation(numBytes32, &allocatedLength);
availableLength = allocatedLength numBytes32;
return firstAvailable + availableLength;
}
如果每個空行之後的第一行是描述下一個代碼塊的注釋,則此方法尤其有效:空白行使注釋更可見。
語句中的空白有助于澄清語句的結構。比較以下兩個語句,其中一個有空格,另一個沒有:
for(int pass=1;pass>=0&&!empty;pass--) {
for (int pass = 1; pass >= 0 && !empty; pass--) {
注釋: 有時不可能避免不明顯的代碼。當這種情況發生時,通過提供缺失的資訊來使用注釋進行補償是很重要的。為了做到這一點,你必須站在讀者的立場上,弄清楚什麼可能會讓他們感到困惑,什麼資訊會消除這種困惑。下一節将展示一些示例。
有許多事情會使代碼變得不明顯;本節提供一些示例。
其中一些方法(如事件驅動程式設計)在某些情況下是有用的,是以您最終可能會使用它們。當這種情況發生時,額外的文檔可以幫助減少讀者的困惑。
事件驅動的程式設計。在事件驅動程式設計中,應用程式響應外部事件,如網絡包的到來或按下滑鼠按鈕。一個子產品負責報告傳入的事件。應用程式的其他部分通過請求事件子產品在事件發生時調用給定的函數或方法來注冊特定事件。
事件驅動的程式設計使跟蹤控制流變得很困難。事件處理函數從不直接調用;它們由事件子產品間接調用,通常使用函數指針或接口。即使您在事件子產品中找到了調用點,仍然無法判斷将調用哪個特定函數:這将取決于在運作時注冊了哪些處理程式。是以,很難對事件驅動的代碼進行推理,也很難說服自己它是有效的。
為了彌補這種模糊,可以使用每個處理函數的接口注釋來訓示何時調用它,如下例所示:
/**
* This method is invoked in the dispatch thread by a transport if a
* transport-level error prevents an RPC from completing.
*/
void
Transport::RpcNotifier::failed() {
...
}
危險信号:不明顯的代碼
如果不能通過快速閱讀了解代碼的含義和行為,這是一個危險信号。通常這意味着有一些重要的資訊對于閱讀代碼的人來說不是很清楚。
通用的容器: 許多語言提供了将兩個或多個項分組成一個對象的泛型類,例如Java中的Pair或c++中的std:: Pair。這些類很有吸引力,因為它們使傳遞帶有單個變量的多個對象變得很容易。最常見的用法之一是從一個方法傳回多個值,就像在這個Java示例中:
return new Pair<Integer, Boolean>(currentTerm, false);
不幸的是,泛型容器會導緻不明顯的代碼,因為分組的元素具有泛型名稱,進而模糊了它們的含義。在上面的示例中,調用者必須使用result.getKey()和result.getValue()引用兩個傳回的值,這兩個值對值的實際含義沒有任何提示。
是以,最好不要使用通用容器。如果需要容器,請定義專門用于特定用途的新類或結構。然後可以為元素使用有意義的名稱,還可以在聲明中提供額外的文檔,這對于通用容器是不可能的。
這個例子說明了一個普遍的規則:軟體應該設計為易于閱讀,而不是易于編寫。 對于編寫代碼的人來說,泛型容器是一種權宜之計,但是它們會給後面的讀者帶來混亂。編寫代碼的人最好多花幾分鐘來定義一個特定的容器結構,這樣得到的代碼就會更明顯。
聲明和配置設定的不同類型。考慮以下Java示例:
private List<Message> incomingMessageList;
...
incomingMessageList = new ArrayList<Message>();
變量被聲明為一個清單,但是實際的值是一個ArrayList。這段代碼是合法的,因為List是ArrayList的一個超類,但是它會誤導那些隻看到聲明而沒有看到實際配置設定的讀者。實際類型可能會影響變量的使用方式(與List的其他子類相比,arraylist具有不同的性能和線程安全屬性),是以最好将聲明與配置設定比對起來。
違反讀者期望的代碼。考慮以下代碼,它是Java應用程式的主程式:
public static void main(String[] args) {
...
new RaftClient(myAddress, serverAddresses);
}
大多數應用程式在它們的主程式傳回時退出,是以讀者可能會認為這将在這裡發生。然而,事實并非如此。RaftClient的構造函數建立了額外的線程,即使應用程式的主線程已經結束,這些線程仍然繼續運作。這種行為應該被記錄在RaftClient構造函數的接口注釋中,但是這種行為還不夠明顯,值得在main的末尾加上簡短的注釋。注釋應該表明應用程式将繼續在其他線程中執行。如果代碼符合讀者期望的約定,那麼它就是最明顯的;如果沒有,那麼記錄這種行為很重要,這樣讀者就不會感到困惑。
另一種思考簡單易解性的方式是資訊。如果代碼不明顯,這通常意味着有關于代碼的重要資訊讀者沒有得到:在RaftClient示例中,讀者可能不知道RaftClient構造函數建立了新線程;在結對示例中,讀者可能不知道result.getKey()傳回目前項的編号。
為了使代碼更容易了解,您必須確定讀者始終擁有他們需要的資訊。有三種方法可以做到這一點:
- 最好的方法是使用抽象和消除特殊情況等設計技術來減少所需的資訊量。
- 其次,您可以利用讀者在其他上下文中已經獲得的資訊(例如,通過遵循約定和遵從期望),這樣讀者就不必為您的代碼學習新的資訊。
- 第三,您可以使用良好的名稱和政策注釋等技術,在代碼中向他們顯示重要的資訊。