1. 程式人生 > 實用技巧 >《整潔架構之道》讀書筆記(一)原則

《整潔架構之道》讀書筆記(一)原則

《整潔架構之道》,大作,力薦。原著大概可以分為原則、策略、細節三部分,本博文總結前兩部分,第三部分多為對第二部分中論點的進一步闡述,詳見原著。

Part1 總覽

1.現象:為什麼軟體開發越來效率越低?

程式設計師沒有偷懶。真正偷懶的地方在於:持續低估好的、設計良好的、整潔的程式碼。

不要迷信開發完再重構:

  1. 爛程式碼隨時有可能讓開發團隊陷入困境
  2. 重構往往只是美好幻想:新任務壓力,回顧整個系統的額外成本等等,依靠開發者自覺性去做這件事往往不現實

因此,無論長期或短期,隨心所欲、不加設計的所謂敏捷開發,其實比循規蹈矩還慢。

想要跑得快,先要跑得穩。

另:關於重寫以挽救一個系統,之前是一團亂麻,沒有理由相信同樣的團隊再來一遍就會更好。

2.目標:最小的人力成本實現開發+維護

這裡的最小人力成本是指持續的低成本。

3.軟體系統價值

軟體系統價值包括:

  1. 行為價值,指需求文件中的功能,以及bug修復等
  2. 架構價值,指軟體本身的靈活性(software),需求變更時必須容易被修改,如果系統架構設計時偏向某種特定的“形狀”,那麼修改就會變得困難。因此好的架構應當儘量與“形狀”無關。

3.1為什麼架構價值更重要

推證如下:倘若系統可以執行但無法修改,那麼一旦需求變更,該程式價值開發完後是100,變化來臨時瞬間從100降為0;但如果系統目前無法執行,但易於修改,那麼改好它,並隨需求變化隨之變化,那麼其價值會從0逐步開發到100,並隨需求變動在100附近浮動。

業務部門往往認為行為價值更重要,也更直觀:“完成現有的功能比著眼未來的靈活性更實際”,但當事後業務部門提出改動而我們估算的工作量遠超其預期時,他們必定責怪我們當時放任系統如此混亂。

業務部門完全沒有能力估算架構價值,因為這是開發者的任務,因此將兩個價值比較並尋找平衡點也是開發者的職責之一。

3.2架構價值在緊急-重要矩陣中的位置

我們往往會犯的錯誤是將位序3:不重要但緊急的事情提到位序1去做。重要的事情往往總是不那麼緊急,於是一再拖延,永遠排不到優先順序。

為此,開發者需要長期抗爭,從架構價值出發,與其他部門協作尋找兩種價值的平衡點,這正是開發者的職責所在,也是最容易失職之處。

Part2 正規化

1.結構化正規化

禁用goto,結構化程式設計對程式控制權的直接轉移進行了限制和規範。

2.面向物件程式設計

用多型限制了對指標的直接使用,對程式控制權的間接轉移進行了限制和規範

3.函數語言程式設計

λ表示式使相同輸入必有相同輸出(入參在表示式中是不可變的),限制了賦值操作。

Part3 設計原則

1.SRP: THE SINGLE RESPONSIBILITY PRINCIPLE

單一職責原則。

並非每個模組只能做一件事,而是每個模組應當有且只有一個被修改的原因,對相同的利益相關者負責。(警惕看似可以複用,卻需要向不同方向演進的實體物件)

2.OCP: THE OPEN-CLOSED PRINCIPLE

開閉原則,“設計良好的計算機軟體應該易於擴充套件,同時抗拒修改”。

換句話說,一個設計良好的計算機系統應該在不需要修改的前提下就可以輕易被擴充套件。用於控制依賴的方向與資訊的可見性,組織模組間依賴關係,使得高階元件不會因低階元件被修改而受到影響。

3.LSP: THE LISKOV SUBSTITUTION PRINCIPLE

里氏替換原則。令子類共用函式簽名以實現自由替換(不需要額外增加檢測機制的那種)

4.ISP: THE INTERFACE SEGREGATION PRINCIPLE

介面隔離原則。通過增加一層interface,對子類的修改就無需令其呼叫方重新編譯和部署。這可以使高層元件無需依賴其並不關心的實現細節。

5.DIP: THE DEPENDENCY INVERSION PRINCIPLE

依賴反轉原則。介面要比其實現穩定,因此有如下規約:

  1. 應在程式碼中多使用抽象介面,避免使用那些多變的具體實現類
  2. 不要在具體實現類上建立衍生類
  3. 不要覆蓋(override)包含具體實現的函式。
  4. 避免在程式碼中寫入與任何具體實現相關的名字,或者是其他容易變動的事物的名字

DIP也是劃分元件邊界的重要方式。

Part4 元件設計原則

1.元件

無論採用什麼程式語言來開發軟體,元件都是該軟體在部署過程中的最小單元,例如.jar, .dll, .exe等。良好的元件應當永遠能夠被獨立部署,單獨開發。

2.元件發展歷史

2.1 指定記憶體地址

早期儲存裝置十分昂貴,記憶體十分有限,無法將全部程式一次讀取到記憶體中,因此會將庫函式單獨編譯到指定位置,這樣程式就可以僅儲存關於庫函式的符號表以引用庫函式。這做法解決了初期的問題,但隨著程式規模發展,庫函式擴大,其記憶體地址也需要重新劃分。

2.2 重定位

引入一個智慧載入器,自動將庫函式載入到適合的位置並將起始位置記錄,需要修改時同步修改。這樣我們就可以將其按使用順序載入進記憶體,再逐個重定位,以使我們能只加載我們實際使用的程式。

另一處對編譯器的修改在於,程式設計師將函式名作為元資料儲存起來,使用時如果讀取到庫函式,就會將其作為外部引用與元資料連線在一起。這就是連結載入器的由來。

2.3聯結器

之後,程式規模不斷增長,使用連結載入器實在太慢,程式設計師們將載入和連結過程分離,將連結過程放到單獨的程式中執行,即“連結器”。再隨後,又出現了能夠快速載入的可執行檔案。

但這一些速度的提升也很快被程式規模的增長填滿。

2.4墨菲定律與摩爾定律

程式規模的墨菲定律:程式的規模會一直不斷地增長下去,直到將有限的編譯和連結時間填滿為止。

摩爾定律:當價格不變時,積體電路上可容納的電晶體數目,約每隔18個月便會增加一倍,效能也將提升一倍。

這兩個定律的較量最終以摩爾定律獲勝而終,我們終於不用跟程式的讀取方式較勁了,甚至可以實現實時連結。

3.元件聚合

哪些應當被組織為同一個元件?該問題經常被草率地拍腦門決策。

3.1REP:The Reuse/Release Equivalence Principle

複用/釋出等同原則:軟體複用的最小粒度應當等同於釋出的最小粒度。

這樣可以方便通過版本號進行管理,使用方可以通過釋出文件決定使用新版本或者舊版本。

  1. 同時一起釋出說明該元件中的類不可毫無關聯,應當有一個共同的主題或方向;

  2. 同一組件的類共享版本號、版本路徑、釋出文件,這要求同一元件的內容應當同時對作者和使用者“有意義”,究竟怎樣才算有意義需要由CCP和CRP補充說明。

3.2 CCP:The Common Closure Principle

共同閉包原則:會同時修改、為相同目的修改的東西應當放到同一個元件,否則就應當放到不同元件裡。

相當於SRP原則在元件層面的闡述,一個元件也不該存在多個變更原因。這樣當出現需求變動時,我們就可以僅需重新部署一個元件了。

這個原則非常需要經驗與預測能力以決定閉包範圍。

3.3CRP:The Common Reuse Principle

共同複用原則:不可強迫使用者依賴他們所不需要的東西。

該原則告訴我們應該把哪些類分開。因為當一個類保有另一個元件的引用時,就在這兩個類之間增加了一條依賴關係。被引用的元件發生變更時,引用它的元件也不得不隨之一起變更。就算它程式碼沒動過,也需要重新編譯、部署等等來上一套。

因此我們不希望出現僅依賴於另一元件中個別類的情況,這會造成後續很多不必要的部署工作。換句話說,不是緊密相連的類不該放在同一組件中。

3.4元件聚合張力圖

至此可見,上述三原則實際上是競爭關係,軟體架構師的工作之一就是在這三者之間權衡、取捨。REP和CCP會使元件趨向於變大,而CRP則趨向於使元件變小。

張力圖

早期一般起於右上角犧牲複用(前期系統較小而且追求釋出速度),而後逐漸發展,其他專案對其產生依賴,會向左側發展。

4.元件耦合

4.1 避免環狀依賴

元件依賴關係圖中不可以存在環狀依賴。環狀依賴嚴重影響協同開發,同事的改動傳遞到自己負責的元件上而導致各種問題。這種現象代價很高,從測試到維護都會影響到,尤其發展到一定程度的專案。解決手段有:

1.藉助依賴反轉手段除去環狀依賴

2.將依賴涉及到的東西提取成單獨的元件,然後兩方都去依賴這個新元件

此外需要額外加一些說明。

首先,兩種手段所造成的結構變化導致我們無法在最初定死專案的元件結構。

其次,元件依賴關係圖主旨並不在闡述系統功能,更貼近於一張系統構建部署維護的地圖。

4.2 SDP: STABLE DEPENDENCIES PRINCIPLE

穩定依賴原則:依賴關係應當指向更穩定的方向。

穩定與否判斷標準在於令其變動所需付出的努力。類比硬幣,立起來的硬幣雖然靜止但一碰就倒,這就是不穩定。對應軟體行業,我們用扇入(別組件依賴於它)&扇出(它依賴於別組件)來衡量穩定性:

I=扇出/(扇入+扇出),越接近0越穩定。

設計依賴方向圖時應當使得被依賴元件I值更小。另外,並非所有元件都是I越小越好(例如Main元件)

4.3 SAP: STABLE ABSTRACTIONS PRINCIPLE

穩定抽象原則:抽象程度於穩定程度應當成正比。

依賴關係應當指向更抽象的方向,一方面將高層決策與具體實現分離,另一方面避免高層決策散落在具體實現中導致框架難以變動。

對於抽象程度的衡量,我們可以用:

A=Na/Nc 即元件中抽象class數/全部class的值判斷,越接近於1則抽象程度越高。

4.4 主序列

通過I和A,我們可以衡量元件設計的優劣。

主序列矩陣

(0,0)附近表示既穩定又具體,這說明它被眾多元件依賴但卻難以改動,那麼一旦確認需要對其進行更改,就會是一場噩夢。例如資料庫,每次升級都非常麻煩;再如工具類,很難想象String發生變化會怎樣,因此僅有完全確定它絕對不可能變動才會放在這裡,但能在最開始就確認完全不會變動的東西少之又少。

(1,1)附近表示既不穩定又抽象,雖然抽象程度夠了但卻沒什麼元件鳥它,顯然,它沒什麼用。

而除開這兩塊區域即為合理。最優的設計會將元件貼近主序列線的兩端,但這僅僅是理想情況,實際操作中只要貼近這條線就ok的。