軟件設計的哲學:第二章 復雜性的本質


作者簡介:常柱,微信公眾號【架構未來】作者,十多年一線互聯網研發從業經驗;前五八同城商業會員技術負責人,寶駕租車技術總監,現58到家業務中台技術負責人。

這本書是關於如何設計軟件系統來最小化它們的復雜性。第一步是了解敵人。到底什么是“復雜性”?你怎么知道一個系統是不是不必要的復雜?什么導致系統變得復雜?本章將在較高的層次上討論這些問題;接下來的章節將向您展示如何在較低的層次上,根據特定的結構特征來識別復雜性。

**識別復雜性的能力是一項重要的設計技能。**它允許您在投入大量精力之前識別問題,並且允許您在備選方案中做出正確的選擇。判斷一個設計是否簡單要比創建一個簡單的設計容易,但是一旦你認識到一個系統太復雜了,你就可以用這種能力來引導你的設計哲學走向簡單。如果一個設計看起來很復雜,嘗試一種不同的方法,看看是否更簡單。隨着時間的推移,您會注意到某些技術往往會促成更簡單的設計,而其他技術則與復雜性相關。這將引導你您更快地生成更簡單的設計。

本章還列出了一些基本假設,為本書的其余部分奠定了基礎。后面的章節采用本章的材料,並使用它來證明各種改進和結論。

2.1定義的復雜性

出於本書的目的,我以一種實際的方式定義了“復雜性”。復雜性是與軟件系統的結構有關的任何東西,它使系統難於理解和修改。 復雜性可以有多種形式。例如,可能很難理解一段代碼是如何工作的;實現一個小的改進可能需要很多的努力,或者可能不清楚必須修改系統的哪些部分才能實現改進;如果不引入另一個bug,可能很難修復一個bug。如果一個軟件系統難以理解和修改,那么它就是復雜的;如果它容易理解和修改,那么它就是簡單的。

你也可以從成本和收益的角度來考慮復雜性。在一個復雜的系統中,即使是很小的改進也需要大量的工作來實現。在一個簡單的系統中,更大的改進可以用更少的努力實現。

復雜性是開發人員在試圖實現特定目標時在特定時間點所經歷的。它不一定與系統的總體大小或功能相關。人們經常使用“復雜”這個詞來描述具有復雜功能的大型系統,但是如果這樣的系統易於操作,那么,就本書的目的而言,它並不復雜。當然,幾乎所有大型和復雜的軟件系統實際上都很難操作,所以它們也符合我對復雜性的定義,但情況不一定如此。一個小而不復雜的系統也可能變得相當復雜。

復雜性是由最常見的活動決定的。如果一個系統有一些非常復雜的部分,但是這些部分幾乎不需要被觸及,那么它們對系統的整體復雜性沒有太大的影響。用粗糙的數學方法來描述它:

 

 

一個系統的總體復雜度(C)是由每個部分p (cp)的復雜度決定的,而每個部分p (cp)的復雜度是由開發人員在該部分(tp)上花費的時間所占的比例決定的。將復雜性隔離在一個永遠不會被看到的地方,幾乎與完全消除復雜性一樣好。

讀者比作者更能感受到復雜性。如果你寫了一段代碼,你覺得它很簡單,但別人卻認為它很復雜。當您發現自己處於這種情況時,有必要研究一下其他開發人員,看看為什么代碼對他們來說比較復雜;從你的觀點和他們的觀點之間的脫節中可能會學到一些有趣的教訓。作為開發人員,您的工作不僅是創建可以輕松使用的代碼,而且還要創建其他人也可以輕松使用的代碼。

2.2復雜性的症狀

復雜性一般表現在三個方面,在下文各段加以說明。每一種表現都使執行開發任務變得更加困難。

變更放大: 復雜性的第一個症狀是,一個看似簡單的變更需要在許多不同的地方修改代碼。例如,考慮一個包含多個頁面的Web站點,每個頁面都顯示帶有背景顏色的橫幅。在許多早期的Web站點中,在每個頁面上都顯式地指定了顏色,如圖2.1(a)所示。為了更改這樣一個Web站點的背景,開發人員可能必須手工修改每個現有頁面;對於一個擁有數千個頁面的大型站點來說,這幾乎是不可能的。幸運的是,現代Web站點使用類似於圖2.1(b)的方法,其中在中心位置指定了一次banner顏色,並且所有單獨的頁面都引用了共享的值。使用這種方法,只需一次修改就可以更改整個Web站點的橫幅顏色。好的設計的目標之一是減少每個設計決策所影響的代碼量,因此設計更改不需要太多的代碼修改。

認知負荷: 復雜性的第二個症狀是認知負荷,指的是開發人員為了完成一項任務需要知道多少。較高的認知負荷意味着開發人員必須花費更多的時間來學習所需的信息,而且由於他們錯過了一些重要的內容,因此存在更大的bug風險。例如,假設C中的一個函數分配內存,返回一個指向該內存的指針,並假設調用者將釋放內存。這增加了使用該函數的開發人員的認知負荷;如果開發人員未能釋放內存,就會出現內存泄漏。如果系統可以重新構造,這樣調用者就不必擔心釋放內存的問題(分配內存的同一個模塊也負責釋放內存),那么將減少認知負荷。認知負荷以多種方式出現,如api具有多種方法、全局變量、不一致性和模塊之間的依賴性。

 

 

系統設計者有時認為復雜性可以用代碼行來度量。他們認為,如果一個實現比另一個短,那么它一定更簡單;如果只需要幾行代碼就可以進行更改,那么更改必須很容易。然而,這種觀點忽略了與認知負荷相關的成本。我曾見過一些框架,它們只允許用幾行代碼來編寫應用程序,但要弄清這些代碼是什么卻非常困難。有時需要更多行的代碼的方法實際上更簡單,因為它減少了認知負荷。

圖2.1:Web站點中的每個頁面都顯示一個彩色的橫幅。在(a)中,旗幟的背景顏色在每個頁面中明確指定。在(b)中,共享變量保存背景顏色,每個頁面引用該變量。在(c)部分網頁顯示另一種顏色以作強調,即為橫額背景顏色的較暗色調;如果背景顏色改變了,強調的顏色也必須改變。

未知的未知: 復雜性的第三個症狀是,必須修改哪些代碼才能完成任務,或者開發人員必須獲得哪些信息才能成功地執行任務,這些都是不明顯的。圖2.1(c)說明了這個問題。網站使用一個中心變量來確定橫幅的背景顏色,所以它看起來很容易改變。但是,一些Web頁面使用較暗的背景色來強調,並且在各個頁面中明確指定了較暗的顏色。如果背景顏色改變,那么強調的顏色必須改變以匹配。不幸的是,開發人員不太可能意識到這一點,所以他們可能會更改中央bannerBg變量而不更新強調顏色。即使開發人員意識到這個問題,也不清楚哪些頁面使用了強調色,因此開發人員可能必須搜索Web站點中的每個頁面。

在復雜性的三種表現形式中,未知的未知是最糟糕的。一個未知的未知意味着你需要知道一些事情,但是你沒有辦法找到它是什么,甚至是否有一個問題。你不會發現它,直到錯誤出現后,你做了一個改變。更改放大是令人惱火的,但是只要清楚哪些代碼需要修改,一旦更改完成,系統就會工作。同樣,高的認知負荷會增加改變的成本,但如果明確要閱讀哪些信息,改變仍然可能是正確的。對於未知的未知,不清楚該做什么,或者提出的解決方案是否有效。唯一確定的方法是讀取系統中的每一行代碼,這對於任何大小的系統都是不可能的。甚至這可能還不夠,因為更改可能依賴於一個從未記錄的細微設計決策。

好的設計最重要的目標之一就是讓系統變得顯而易見。 這與高認知負荷和未知的未知相反。在一個明顯的系統中,開發人員可以快速地理解現有代碼是如何工作的,以及需要做哪些更改。一個明顯的系統是這樣的:開發人員可以快速地猜測應該做什么,而不需要非常仔細地思考,並且確信猜測是正確的。第18章討論了使代碼更明顯的技術。

2.3 復雜性的原因

既然您已經了解了復雜性的高級症狀,以及復雜性使軟件開發變得困難的原因,那么下一步就是了解導致復雜性的原因,這樣我們就可以設計系統來避免這些問題。復雜性是由兩件事引起的:依賴性和模糊性。本節從較高的層次討論這些因素;后續章節將討論它們如何與底層設計決策相關聯。

對於本書而言,當不能獨立地理解和修改給定的代碼段時,就會存在依賴項;代碼在某種程度上與其他代碼相關,如果給定代碼發生更改,則必須考慮和/或修改其他代碼。在圖2.1(a)的Web站點示例中,背景顏色創建了所有頁面之間的依賴關系。所有的頁面都需要有相同的背景,所以如果一個頁面的背景被改變了,那么所有的頁面都必須被改變。另一個依賴的例子發生在網絡協議中。通常協議有單獨的發送方和接收方代碼,但它們必須各自遵守協議;更改發送方的代碼幾乎總是需要在接收方進行相應的更改,反之亦然。方法的簽名在該方法的實現和調用它的代碼之間創建了一個依賴關系:如果向方法添加了一個新參數,則必須修改該方法的所有調用來指定該參數。

依賴是軟件的基本組成部分,不能完全消除。事實上,我們有意將依賴關系作為軟件設計過程的一部分。每次編寫新類時,都要圍繞該類的API創建依賴項。然而,軟件設計的目標之一是減少依賴項的數量,並使依賴項盡可能簡單和明顯。

以Web站點為例。在每個頁面上單獨指定背景的舊Web站點中,所有Web頁面都是相互依賴的。新的Web站點通過在中心位置指定背景顏色並提供一個API來修復這個問題,這個API用於各個頁面在呈現時檢索該顏色。新的Web站點消除了頁面之間的依賴關系,但是圍繞API創建了一個新的依賴關系來檢索背景顏色。幸運的是,新的依賴關系更加明顯:很明顯,每個單獨的Web頁面都依賴於bannerBg顏色,開發人員可以通過搜索變量名輕松地找到該變量使用的所有位置。此外,編譯器還有助於管理API依賴關系:如果共享變量的名稱發生更改,則在仍然使用舊名稱的任何代碼中都會出現編譯錯誤。新的Web站點用一個更簡單、更明顯的依賴項代替了一個不明顯、難於管理的依賴項。

復雜性的第二個原因是晦澀。 當重要的信息不明顯時,就會發生模糊。一個簡單的例子是一個變量名,它是如此的通用,以至於它沒有攜帶太多有用的信息(例如,時間)。或者,一個變量的文檔可能沒有指定它的單位,所以找到它的惟一方法是掃描代碼,查找變量使用的位置。晦澀常常與依賴項相關,而依賴項的存在並不明顯。例如,如果向系統添加了一個新的錯誤狀態,可能需要向一個包含每個狀態的字符串消息的表添加一個條目,但是對於查看狀態聲明的程序員來說,消息表的存在可能並不明顯。不一致性也是造成不透明性的一個主要原因:如果同一個變量名用於兩個不同的目的,那么開發人員就無法清楚地知道特定變量的作用。

在許多情況下,含糊不清是因為文檔不充分;第13章討論這個主題。然而,晦澀也是一個設計問題。如果一個系統有一個清晰而明顯的設計,那么它將需要更少的文檔。需要大量的文檔通常是設計不太正確的一個危險信號。減少模糊的最佳方法是簡化系統設計。

相關性和模糊性共同解釋了2.2節中描述的復雜性的三種表現形式。依賴會導致變化放大和高認知負荷。晦澀創造了未知的未知,也增加了認知負荷。如果我們能找到最小化依賴和模糊的設計技術,那么我們就能降低軟件的復雜性。

2.4 復雜性是遞增的

復雜性不是由單個災難性錯誤造成的,而是由許多小問題積累而成。 單個依賴項或晦澀本身不太可能對軟件系統的可維護性產生重大影響。復雜性的產生是因為成百上千的小的依賴和模糊隨着時間的推移而累積。最終,這些小問題如此之多,以至於對系統的每一個可能的更改都會受到其中幾個問題的影響。

復雜性的遞增性使其難以控制。 很容易讓自己相信,您當前的更改所引入的一點復雜性並不是什么大問題。但是,如果每個開發人員對每個更改都采用這種方法,那么復雜性會迅速增加。一旦復雜性積累起來,就很難消除,因為修復單個依賴項或晦澀本身不會產生很大的影響。為了減緩復雜性的增長,您必須采用“零容忍”的哲學,如第3章所述。

2.5 結論

復雜性來自於依賴和模糊的積累。隨着復雜性的增加,它會導致變化的擴大、高的認知負荷和未知的未知。 因此,需要進行更多的代碼修改來實現每個新特性。此外,開發人員花費更多的時間來獲取足夠的信息以安全地進行更改,在最壞的情況下,他們甚至無法找到所需的所有信息。底線是,復雜性使得修改現有代碼庫變得困難和危險。


免責聲明:本文翻譯僅供學習使用,本文的版權歸英文原作者或出版方,若有侵權,請聯系刪除。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM