第 16 章 修改現有的程式碼
第 1 章介紹了軟體開發是如何迭代和增量的。大型軟體系統是透過一系列演化階段開發的,其中每個階段都添加了新功能並修改了現有模組。這意味著系統的設計在不斷演進。不可能從一開始就為系統構思出正確的設計。一個成熟系統的設計更多地取決於系統演化過程中所做的更改,而不是初始的概念。前面的章節描述瞭如何在初始設計和實現過程中降低複雜性。本章討論如何防止複雜性隨著系統的演進而蔓延。
16.1 保持戰略式的思考
第 3 章介紹了戰術式程式設計和戰略式程式設計之間的區別:在戰術式程式設計中,主要目標是使某些事物快速工作,即使這會導致額外的複雜性;而在戰略式程式設計中,最重要的目標是進行出色的系統設計。戰術式的方法很快會導致系統設計混亂。如果您想要一個易於維護和擴充套件的系統,那麼“能工作的”並不是一個足夠高的標準。您必須優先考慮設計並從戰略角度進行思考。當您修改現有的程式碼時,此想法也是適用的。
不幸的是,當開發人員對現有程式碼進行更改(例如缺陷修復或加入新功能)時,他們通常不會從戰略角度進行思考。一種典型的心態是“實現該功能,我能做出的最小改變是什麼?” 有時開發人員認為這是合理的,因為他們對修改的程式碼不放心。他們擔心較大的更改會帶來更大的風險,會引入新的缺陷。然而,這導致了戰術式的程式設計。每一個最小的變化都會引入一些特殊情況、依賴性或其他形式的複雜性。結果,系統設計變得更糟了一點,並且問題隨著系統演進的每一步而累積。
如果要保持系統的簡潔設計,則在修改現有程式碼時必須採取戰略式的方法。理想情況下,當您完成每次更改時,系統的結構將像是在最開始的設計中就考慮了這個更改。 為了實現此目標,您必須抵制快速解決問題的誘惑。相反,請根據所需的更改來考慮當前的系統設計是否仍然是最佳的。如果不是,請重構系統,以便最終獲得最佳設計。透過這種方法,每次修改都會持續改善系統設計。
這也是第 3.2 節介紹的投資思維的一個示例:如果您花費一些額外的時間來重構和改善系統設計,您將得到一個更整潔的系統。這將加快開發速度,您將收回在重構方面投入的精力。即使您的特定更改不需要重構,您仍然應該注意在程式碼中可以修復的設計缺陷。每當您修改任何程式碼時,都嘗試在該過程中至少找到一些改進系統設計的地方。如果您沒有使設計變得更好,則您有可能會使它變得更糟。
如第 3 章所述,投資思維有時與商業軟體開發的現實相沖突。如果以“正確的方式”重構系統需要三個月,而快速且不整潔的修復僅需兩個小時,則您可能必須採取快速而不整潔的方法,尤其是當您被要求在緊張的期限內完成工作時。或者,如果重構系統會造成不相容,從而影響許多其他的人員和團隊,則這個重構可能有些不切實際。
儘管如此,您應儘可能抵制這些妥協。問問自己:“考慮到我目前的限制,這是否是我能做的最好的工作來建立一個整潔的系統設計?” 也許有一種替代方法幾乎可以像 3 個月的重構一樣整潔,但是可以在幾天內完成?或者,如果您現在沒有能力做大規模的重構,請讓您的老闆為您分配時間,讓您在當前的截止日期之後再來做。每個開發組織都應計劃將其全部工作的一小部分用於清理和重構;從長遠來看,這項工作一定是物有所值的。
16.2 維護註釋:將註釋保留在程式碼附近
當您更改現有程式碼時,更改很有可能會使某些現有的註釋失效。修改程式碼時,也很容易忘記更新註釋,從而導致註釋不再準確。陳舊的註釋使讀者感到沮喪,如果有很多這樣的註釋,讀者就會開始不信任所有註釋。幸運的是,只要有一點紀律和一些指導規則,就可以在不需要大投入的情況下使註釋保持更新。本節及隨後的部分提出了一些具體的技巧。
確保註釋更新的最佳方法是將註釋放置在它們所描述的程式碼附近,以便開發人員在更改程式碼時可以看到它們。註釋離其關聯的程式碼越遠,被正確更新的可能性就越小。例如,方法的介面註釋的最佳位置是在程式碼檔案中,緊靠該方法主體的位置。對方法的任何更改都將涉及此程式碼,因此開發人員很可能會看到介面註釋,並在需要時進行更新。
對於 C 和 C++ 等具有單獨的程式碼和標頭檔案的語言,一種替代方法是將介面註釋放在 .h
檔案中方法宣告的旁邊。但是,這距離程式碼還有很長的路要走。開發人員在修改方法的主體時將看不到這些註釋,因此需要開啟其他檔案並查詢介面註釋來更新它們,這需要額外的工作。有人可能會爭辯說介面註釋應該放在標頭檔案中,以便使用者在不檢視程式碼的情況下就能瞭解如何使用這個抽象層。然而,使用者應該不需要閱讀程式碼和標頭檔案;他們應該從由 Doxygen 或 Javadoc 等工具編譯的文件中獲取資訊。此外,許多 IDE 都會提取文件並將其呈現給使用者,例如在鍵入方法名稱時顯示方法的文件。鑑於已經有這樣的工具,文件應位於對開發人員進行程式碼開發最方便的位置。
在編寫實現註釋時,不要將整個方法的所有註釋放在方法的頂部。把他們分解開來,將每個註釋向下寫到最合適的範圍,即包括該註釋所引用的所有程式碼的範圍。例如,如果一種方法具有三個主要階段,則不要在方法的頂部寫一個詳細描述所有階段的註釋。而是為每個階段編寫一個單獨的註釋,並將該註釋放置在相應階段的第一行程式碼的正上方。另一方面,在方法實現的頂部添加註釋描述總體的策略也可能會有所幫助,例如:
// We proceed in three phases:
// Phase 1: Find feasible candidates
// Phase 2: Assign each candidate a score
// Phase 3: Choose the best, and remove it
更多的細節可以在各個階段程式碼的正上方記錄。
通常,離描述的程式碼越遠,註釋應該越抽象(這減少了註釋因程式碼更改而無效的可能性)。
16.3 註釋屬於程式碼,而不是提交日誌
修改程式碼時,常見的錯誤是將有關更改的詳細資訊放入原始碼儲存庫的提交訊息中,而不是將其記錄在程式碼中。儘管將來可以透過掃描儲存庫的日誌來瀏覽提交訊息,但是需要該資訊的開發人員不太可能知道要檢視儲存庫的日誌。即使他們確實查看了日誌,找到正確日誌的過程也會很乏味。
在編寫提交訊息時,請問問自己:未來的開發人員是否需要使用該資訊?如果是,則應該在程式碼中記錄此資訊。以一個描述了微妙問題導致程式碼變更的提交訊息為例,如果程式碼中未對此進行記錄,那麼開發人員可能會稍後撤消這個更改,而沒有意識到他們已經重新引入了一個缺陷。如果您也想在提交訊息中包含此資訊的副本,那也可以,但是最重要的事情是把它放在程式碼中。這說明了將文件放置在開發人員最有可能看到它的地方的原則,而提交日誌可不是這樣的地方。
16.4 維護註釋:避免重複
確保註釋更新的第二種技術是避免重複。如果文件重複,那麼開發人員將很難找到並更新所有相關副本。因此儘量將每個設計決策精確的記錄一次。如果程式碼中有多個地方受某個特定決策的影響,請不要在所有這些地方重複註釋。而應該找到放置註釋最明顯的位置。例如,假設存在與某個變數相關的棘手行為,這會影響使用變數的幾個不同地方,您可以在變數宣告旁邊的註釋中記錄該行為。如果開發人員在理解使用該變數的程式碼時遇到麻煩,他們自然會在這裡進行檢查。
如果沒有一個“明顯的”地方來將特定的文件放在開發人員可以找到的地方,那麼可以建立一個 designNotes
檔案,如第 13.7 節所述;或者在現有的地方中選擇一個最好的地方,並把文件放在那裡。此外,可以在其它地方新增簡短的註釋以指向中心位置,比如:“檢視 xyz
中的註釋以理解下面的程式碼。”如果引用因為主註釋被移動或刪除而變得過時,這種不一致性將是很明顯的,因為開發人員將無法在指定的位置找到註釋,他們可以使用版本控制歷史記錄來確認註釋發生了什麼事情,並相應地更新引用。相反,如果文件是重複的,而一些副本沒有得到更新,那麼開發人員就不會知道他們使用的是陳舊的資訊。
不要將一個模組的設計決策記錄在另一個模組中。例如,不要在方法呼叫前添加註釋,以解釋被呼叫方法中發生的事情。如果讀者想知道,他們應該檢視該方法的介面註釋。好的開發工具通常會自動提供此資訊,例如,如果您選擇了方法的名稱或將滑鼠懸停在該方法的名稱上,則將顯示該方法的介面註釋。儘量讓開發人員容易找到合適的文件,但是不要透過重複文件來做到這一點。
如果資訊已經在程式之外的某個地方記錄了,不要在程式內部重複記錄,只需引用外部文件。 例如,如果您編寫一個實現 HTTP 協議的類,那麼就不需要在程式碼中描述 HTTP 協議。在網上已經有很多關於這個文件的來源,只需在您的程式碼中新增一個簡短的註釋,並附上其中一個來源的 URL 即可。另一個例子是已經在使用者手冊中記錄的特性。假設您正在編寫一個實現命令集合的程式,其中每個命令都有一個負責實現的方法。如果有描述這些命令的使用者手冊,就不需要在程式碼中重複這些資訊。相反,在每個命令方法的介面註釋中包含如下簡短說明即可:
// Implements the Foo command; see the user manual for details.
讓讀者能輕鬆找到理解程式碼所需的所有文件是很重要的,但這並不意味著您必須編寫所有這些文件。
16.5 維護註釋:檢查待提交的變更
確保文件保持最新狀態還有一個好方法,在將變更提交到版本控制系統之前,花費幾分鐘以檢查該提交的所有變更,並確保文件正確反映了每個變更。這些提交前的檢查還有可能檢測到其他問題,例如意外地將除錯程式碼留在系統中,或者尚未完成的 TODO 專案。
16.6 更高層級的註釋更易於維護
關於文件維護的最後一個想法:如果註釋比程式碼更高層級和更抽象,則註釋更易於維護。這些註釋不會反映程式碼的詳細資訊,因此它們不會受到次要的程式碼更改的影響,只有整體行為的變化才會影響這些註釋。當然,正如第 13 章所討論的那樣,某些註釋的確需要詳細和精確。但總的來說,最有用的註釋(它們不是簡單地重複程式碼)也最容易維護。