第 2 章 複雜性的本質
本書討論如何透過軟體系統的設計儘量減少複雜性。第一步是瞭解我們要對付的物件。究竟什麼是“複雜性”?如何判斷系統是否存在不必要的複雜性?系統為什麼會變得複雜?本章先從整體上回答這些問題,後續章節再深入具體的結構特徵,介紹如何識別複雜性。
本章概覽複雜性是什麼,怎麼看出來,從哪來?
識別複雜性是一項關鍵的設計能力。它能讓你在投入大量精力之前發現問題,也能幫助你在備選方案之間作出合理選擇。判斷一個設計是否簡單,比做出一個簡單的設計容易得多;不過,一旦能看出系統過於複雜,就可以藉此讓設計朝著簡單的方向發展。如果一個設計看起來很複雜,就試試其他方案,看能否更簡單。隨著經驗積累,你會發現某些技術往往能簡化設計,另一些技術則容易伴隨複雜性。這會幫助你更快地做出更簡單的設計。
本章還提出了一些基本假設,為全書其餘內容奠定基礎。後續章節將以此為前提,進一步展開討論並推導結論。
2.1 複雜性的定義
在本書中,我從實用的角度定義“複雜性”:軟體系統結構中,任何使系統難以理解和修改的因素,都屬於複雜性。 複雜性有多種表現形式:一段程式碼的工作方式難以理解,做一點小改進卻要花費大量精力,不清楚該修改系統的哪些部分,或者難以在不引入新問題的前提下修復一個程式碼缺陷。如果一個軟體系統難以理解和修改,它就是複雜的;如果容易理解和修改,它就是簡單的。
你還可以從成本和收益的角度來評估複雜性。在複雜的系統中,即使實施很小的改進都需要大量的工作。而在一個簡單的系統中,可以用更少的精力達成更大的改進。
複雜性是開發人員在某個時刻、試圖完成某專案標時感受到的,不一定取決於系統的整體規模或功能。人們常用“複雜”形容功能繁多的大型系統,但如果這樣的系統很容易開發和維護,按本書的定義,它就不算複雜。當然,實際上幾乎所有功能複雜的大型軟體系統都很難開發和維護,也符合這裡對複雜性的定義,但兩者並沒有必然聯絡。規模小、功能簡單的系統,也可能非常複雜。
複雜性取決於最常進行的開發活動。如果系統中某些部分非常複雜,卻幾乎不需要改動,那麼它們對系統整體複雜性的影響就不大。可以用一個粗略的數學公式表示:
系統的總體複雜性(C),是各部分的複雜性(cp)按開發人員在該部分所花時間佔總開發時間的比例(tp)加權求和的結果。把複雜性隔離到一個永遠不必接觸的地方,幾乎與徹底消除它同樣有效。
相比程式碼作者,讀者更容易察覺程式碼的複雜性。你寫的程式碼即使自己覺得簡單,只要別人覺得複雜,它就是複雜的。遇到這種情況,值得向其他開發人員詳細瞭解:為什麼他們覺得這段程式碼複雜?你與他們的看法為何不同?這種差異中可能蘊含值得學習的東西。開發人員的職責不只是寫出自己容易理解和修改的程式碼,還要讓別人也能輕鬆理解和修改。
譯者圖解先簡化最難的,還是常改的?
2.2 複雜性的症狀
複雜性主要有以下三種表現,每一種都會讓開發任務變得更困難。
變更放大:複雜性的第一個徵兆是,看似簡單的變更卻需要修改許多地方的程式碼。例如,一個網站包含多個頁面,每個頁面都有帶背景色的橫幅。在許多早期網站中,顏色分別在每個頁面中指定,如圖 2.1(a)所示。要更改這種網站的橫幅背景色,開發人員可能必須逐個手動修改現有頁面;對於擁有數千個頁面的大型網站,這幾乎不可能完成。幸運的是,現代網站通常採用圖 2.1(b)所示的方式:在一個集中的位置指定橫幅顏色,所有頁面引用這個共享值。這樣,只需修改一處,就能改變整個網站的橫幅顏色。良好設計的目標之一,就是減少每個設計決策所影響的程式碼量,讓設計變更不必牽動太多程式碼。
認知負荷:複雜性的第二個症狀是認知負荷,即開發人員完成一項任務需要掌握多少資訊。認知負荷越高,學習這些資訊所需的時間就越多,遺漏重要資訊而出錯的風險也越大。例如,某個 C 語言函式分配一塊記憶體,回傳指向它的指標,並假定呼叫者會釋放這塊記憶體。這就增加了使用該函式的開發人員的認知負荷。如果沒有釋放記憶體,就會發生記憶體洩漏。若能調整系統,讓分配記憶體的模組也負責釋放,呼叫者就不必操心,認知負荷也會隨之降低。(認知負荷有多種來源,例如包含大量方法的 API、全域變數、不一致的做法和模組間的依賴。)
系統設計人員有時會用程式碼行數衡量複雜性。他們認為,實作越短就越簡單,只改幾行程式碼就一定很容易。但這種觀點忽略了認知負荷帶來的成本。我見過一些框架,用幾行程式碼就能寫出應用程式,可要弄清楚這幾行該怎麼寫卻極其困難。有時,程式碼行數更多的方案反而更簡單,因為它減輕了認知負荷。
譯者圖解誰必須知道何時釋放記憶體?
圖 2.1:網站中的每個頁面都顯示一個彩色橫幅。在(a)中,橫幅的背景色在每個頁面中都明確指定。在(b)中,共享變數儲存背景色,並且每個頁面都引用該變數。在(c)中,某些頁面會顯示其他用於強調的顏色,即橫幅背景顏色的暗色;如果背景顏色改變,則強調顏色也必須改變。
未知的未知:複雜性的第三個症狀是,開發人員看不出完成任務需要修改哪些程式碼,或需要掌握哪些資訊。圖 2.1(c)說明了這個問題。網站用一個中心變數確定橫幅背景色,看起來很容易修改。但有些頁面還使用比背景色更暗的顏色來突出顯示內容,並在各自的頁面中單獨指定這些顏色。背景色改變時,這些強調色也必須相應改變。不幸的是,開發人員未必會意識到這一點,可能只修改中心變數 bannerBg,而沒有更新強調色。即使意識到了問題,也不清楚哪些頁面使用了強調色,因而可能需要搜尋網站的每個頁面。
在複雜性的三種表現中,未知的未知最糟糕。你需要知道某些資訊,卻無從得知它們是什麼,甚至不知道問題是否存在,要等到修改後出現程式碼缺陷才會發現。變更放大固然令人煩惱,但只要知道該改哪些程式碼,改完後系統就能工作。認知負荷高也會增加變更成本,但如果知道需要閱讀哪些資訊,變更仍很可能是正確的。遇到未知的未知時,你既不清楚該做什麼,也不知道提出的方案是否有效。要確定這一點,唯一的辦法是通讀系統的每一行程式碼,而對於稍具規模的系統,這就不可能做到。即便如此,可能仍不夠,因為變更還可能依賴某個從未記錄的細微設計決策。
良好設計的最重要目標之一就是使系統容易理解,這與高認知負荷和未知的未知相反。在這樣的系統中,開發人員可以快速瞭解現有程式碼的工作方式以及進行更改所需的內容,並可以在不費力思考的情況下快速猜測要做什麼,同時又可以確信該猜測是正確的。第 18 章討論了使程式碼更容易理解的技術。
2.3 複雜性的原因
既然你已經瞭解了複雜性在較高層級的症狀以及為什麼複雜性會使軟體開發變得困難,那麼下一步就是了解導致複雜性的原因,以便我們能設計系統來避免這些問題。複雜性是由兩件事引起的:依賴性和模糊性。本節從較高層級討論這些因素。隨後的章節將討論它們與較低層級的設計決策之間的關係。
就本書而言,當無法孤立地理解和修改給定的一段程式碼時,便存在依賴關係。該程式碼以某種方式與其他程式碼相關,如果更改了給定程式碼,則必須考慮和/或修改其他程式碼。在圖 2.1(a)的網站範例中,背景色在所有頁面之間建立了依賴關係。所有頁面都必須具有相同的背景,因此,如果更改一頁的背景,則必須更改所有背景。依賴關係的另一個範例發生在網路協議中。通常,協議的傳送方和接收方有單獨的程式碼,但是它們必須分別符合協議。更改傳送方的程式碼幾乎總是需要在接收方進行相應的更改,反之亦然。方法的簽名建立了方法實作方和方法呼叫方之間的依賴關係:如果向方法添加了一個新參數,則必須修改呼叫該方法的程式碼以指定該參數。
依賴關係是軟體的基本組成部分,不能完全消除。實際上,我們在軟體設計過程中有意引入了依賴性。每次編寫新類時,都會圍繞該類的 API 建立依賴關係。但是,軟體設計的目標之一是減少依賴關係的數量,並使依賴關係保持儘可能簡單和明顯。
考慮網站範例。在每個頁面分別指定背景的舊網站中,所有網頁都是相互依賴的。新的網站透過在中心位置指定背景色並提供一個 API,供各個頁面在呈現它們時檢索該顏色,從而解決了該問題。新的網站消除了頁面之間的依賴關係,但是它圍繞 API 建立了一個新的依賴關係以檢索背景色。幸運的是,新的依賴性更加明顯:很顯然,每個單獨的網頁都取決於 bannerBg 顏色,並且開發人員可以透過搜尋其名稱輕鬆找到使用該變數的所有位置。此外,編譯器還有助於管理 API 依賴性:如果共享變數的名稱發生變化,任何仍使用舊名稱的程式碼都將發生編譯錯誤。新的網站用一種更簡單、更明顯的方式代替了一種不明顯且難以管理的依賴性。
複雜性的第二個原因是模糊性:重要資訊不夠明顯,讓人難以察覺。一個簡單的例子是變數名過於籠統,無法傳達多少有用資訊(例如 time)。又如,變數的文件沒有說明單位,開發人員就只能搜尋程式碼中使用它的位置來判斷。模糊性常與依賴關係有關:依賴關係存在,卻不容易看出來。例如,給系統增加一種錯誤狀態時,可能還需要在儲存各狀態對應訊息的表中新增條目,但只看狀態宣告的程式設計師未必知道這張表的存在。不一致也是模糊性的重要來源:同一個變數名用於兩種不同目的時,開發人員就難以判斷某個具體變數的用途。
譯者圖解看見依賴後,哪些修改仍要做?
在許多情況下,模糊性來源於文件的不足。第 13 章討論了這個主題。但是,模糊性也是設計問題。如果系統設計簡潔明瞭,則所需的文件將更少。對大量文件的需求通常是表明設計不正確的危險訊號。減少模糊性的最佳方法是簡化系統設計。
依賴性和模糊性共同構成了第 2.2 節中描述的三種複雜性表現。依賴性導致變更放大和高認知負荷。模糊性會產生未知的未知,還會增加認知負荷。如果我們找到最小化依賴性和模糊性的設計技術,那麼我們就可以降低軟體的複雜性。
2.4 複雜性是增量產生的
複雜性並非源於某個災難性的錯誤,而是由許多小問題逐漸積累而成。單個依賴關係或一處含糊不清的地方,本身不太可能顯著影響軟體系統的可維護性。但隨著時間推移,成百上千的這類問題不斷積累,最終多到每次修改系統都會受到其中若干問題的影響。
複雜性的增量本質使其難以控制。可以很容易地說服自己,當前更改所帶來的一點點複雜性沒什麼大不了的。但是,如果每個開發人員對每種更改都採用這種方法,那麼複雜性就會迅速累積。一旦積累了複雜性,就很難消除它,因為修復單個依賴項或模糊項本身不會產生很大的變化。為了減緩複雜性的增長,你必須採用第 3 章中討論的“零容忍”理念。
2.5 結論
複雜性來自依賴性和模糊性的積累,並表現為變更放大、較高的認知負荷和未知的未知。因此,每實作一個新功能,都需要修改更多程式碼。開發人員也要花更多時間收集資訊,才能安全地進行變更;最壞的情況下,甚至無法找到全部所需資訊。歸根結底,複雜性讓修改現有程式碼庫變得困難而危險。
