軟件設計的哲學:第十八章 代碼的可見性

晦澀是2.3節中描述的複雜性的兩個主要原因之一。當系統的重要信息對新開發人員來說不明顯時,就會出現模糊現象。模糊問題的解決方案是用一種簡單易解的方式來寫代碼。本章討論了一些使代碼或多或少變得簡單的因素。

如果代碼是簡單易解的,這意味着某人可以快速地閱讀代碼,而不需要太多思考,並且他們對代碼的行爲或含義的第一次猜測將是正確的。如果代碼是簡單易解的,那麼讀者就不需要花費太多時間或精力來收集處理代碼所需的所有信息。如果代碼不是簡單易解的,那麼讀者必須花費大量的時間和精力來理解它。這不僅降低了它們的效率,而且還增加了誤解和錯誤的可能性。明顯的代碼比不明顯的代碼需要更少的註釋。

“簡單易解”是讀者的想法:注意到別人的代碼不簡單易解比看到自己代碼的問題更容易。因此,確定代碼可見性的最佳方法是通過代碼審查。如果有人讀了你的代碼,說它不明顯,那麼它就不明顯,不管它在你看來多麼清楚。通過嘗試理解是什麼使代碼變得不明顯,您將瞭解如何在將來編寫更好的代碼。

18.1 使代碼更簡單的東西

在前幾章中已經討論了使代碼簡單易解的兩個最重要的技術。第一個是選擇好名字 (第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--) {

註釋: 有時不可能避免不明顯的代碼。當這種情況發生時,通過提供缺失的信息來使用註釋進行補償是很重要的。爲了做到這一點,你必須站在讀者的立場上,弄清楚什麼可能會讓他們感到困惑,什麼信息會消除這種困惑。下一節將展示一些示例。

18.2 使代碼不那麼明顯的事情

有許多事情會使代碼變得不明顯;本節提供一些示例。

其中一些方法(如事件驅動編程)在某些情況下是有用的,因此您最終可能會使用它們。當這種情況發生時,額外的文檔可以幫助減少讀者的困惑。

事件驅動的編程。在事件驅動編程中,應用程序響應外部事件,如網絡包的到來或按下鼠標按鈕。一個模塊負責報告傳入的事件。應用程序的其他部分通過請求事件模塊在事件發生時調用給定的函數或方法來註冊特定事件。

事件驅動的編程使跟蹤控制流變得很困難。事件處理函數從不直接調用;它們由事件模塊間接調用,通常使用函數指針或接口。即使您在事件模塊中找到了調用點,仍然無法判斷將調用哪個特定函數:這將取決於在運行時註冊了哪些處理程序。因此,很難對事件驅動的代碼進行推理,也很難說服自己它是有效的。

爲了彌補這種模糊,可以使用每個處理函數的接口註釋來指示何時調用它,如下例所示:

/**

 * 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的末尾加上簡短的註釋。註釋應該表明應用程序將繼續在其他線程中執行。如果代碼符合讀者期望的約定,那麼它就是最明顯的;如果沒有,那麼記錄這種行爲很重要,這樣讀者就不會感到困惑。

18.3 結論

另一種思考簡單易解性的方式是信息。如果代碼不明顯,這通常意味着有關於代碼的重要信息讀者沒有得到:在RaftClient示例中,讀者可能不知道RaftClient構造函數創建了新線程;在結對示例中,讀者可能不知道result.getKey()返回當前項的編號。

爲了使代碼更容易理解,您必須確保讀者始終擁有他們需要的信息。有三種方法可以做到這一點:

  1. 最好的方法是使用抽象和消除特殊情況等設計技術來減少所需的信息量。
  2. 其次,您可以利用讀者在其他上下文中已經獲得的信息(例如,通過遵循約定和遵從期望),這樣讀者就不必爲您的代碼學習新的信息。
  3. 第三,您可以使用良好的名稱和策略註釋等技術,在代碼中向他們顯示重要的信息。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章