天天看點

一文讀懂架構整潔之道(附知識脈絡圖)

程式的世界飛速發展,今天所掌握的技能可能明年就過時了,但有一些東西是曆久彌新,永遠不變的,掌握了這些,在程式的海洋裡就不會迷路,架構思想就是這樣一種東西。

本文是《架構整潔之道》的讀書筆記,文章從軟體系統的價值出發,認識架構工作的價值和目标, 依次了解架構設計的基礎、指導思想(設計原則)、元件拆分的方法和粒度、元件之間依賴設計、元件邊界多種解耦方式以及取舍、降低元件之間通信成本的方法,進而在做出正确的架構決策和架構設計方面,給出作者自己的解讀。

阿裡巴巴中間件微信公衆号對話框,發送“架構”,可擷取《架構整潔之道》知識脈絡圖。直接通路,點選

這裡

一、軟體系統的價值

架構是軟體系統的一部分,是以要明白架構的價值,首先要明确軟體系統的價值。軟體系統的價值有兩方面,行為價值和架構價值。

行為價值是軟體的核心價值,包括需求的實作,以及可用性保障(功能性 bug 、性能、穩定性)。這幾乎占據了我們90%的工作内容,支撐業務先赢是我們工程師的首要責任。如果業務是明确的、穩定的,架構的價值就可以忽略不計,但業務通常是不明确的、飛速發展的,這時架構就無比重要,因為架構的價值就是讓我們的軟體(Software)更軟(Soft)。可以從兩方面了解:

  • 當需求變更時,所需的軟體變更必須簡單友善。
  • 變更實施的難度應該和變更的範疇(scope)成等比,而與變更的具體形狀(shape)無關。

當我們隻關注行為價值,不關注架構價值時,會發生什麼事情?這是書中記錄的一個真實案例,随着版本疊代,工程師團隊的規模持續增長,但總代碼行數卻趨于穩定,相對應的,每行代碼的變更成本升高、工程師的生産效率降低。從老闆的視角,就是公司的成本增長迅猛,如果營收跟不上就要開始賠錢啦。

一文讀懂架構整潔之道(附知識脈絡圖)

可見架構價值重要性,接下來從著名的緊急重要矩陣出發,看我們如何處理好行為價值和架構價值的關系。

重要緊急矩陣中,做事的順序是這樣的:1.重要且緊急 > 2.重要不緊急 > 3.不重要但緊急 > 4.不重要且不緊急。實作行為價值的需求通常是 PD 提出的,都比較緊急,但并不總是特别重要;架構價值的工作内容,通常是開發同學提出的,都很重要但基本不是很緊急,短期内不做也死不了。是以行為價值的事情落在1和3(重要且緊急、不重要但緊急),而架構價值落在2(重要不緊急)。我們開發同學,在低頭敲代碼之前,一定要把雜糅在一起的1和3分開,把我們架構工作插進去。

二、架構工作的目标

前面講解了架構價值,追求架構價值就是架構工作的目标,說白了,就是用最少的人力成本滿足建構和維護該系統的需求,再細緻一些,就是支撐軟體系統的全生命周期,讓系統便于了解、易于修改、友善維護、輕松部署。對于生命周期裡的每個環節,優秀的架構都有不同的追求:

  • 開發階段:元件不要使用大量複雜的腳手架;不同團隊負責不同的元件,避免不必要的協作。
  • 部署階段:部署工作不要依賴成堆的腳本和配置檔案;元件越多部署工作越繁重,而部署工作本身是沒有價值的,做的越少越好,是以要減少元件數量。
  • 運作階段:架構設計要考慮到不同的吞吐量、不同的響應時長要求;架構應起到揭示系統運作的作用:用例、功能、行為設定應該都是對開發者可見的一級實體,以類、函數或子產品的形式占據明顯位置,命名能清晰地描述對應的功能。
  • 維護階段:減少探秘成本和風險。探秘成本是對現有軟體系統的挖掘工作,确定新功能或修複問題的最佳位置和方式。風險是做改動時,可能衍生出新的問題。

三、程式設計範式

其實所謂架構就是限制,限制源碼放在哪裡、限制依賴、限制通信的方式,但這些限制比較上層。程式設計範式是最基礎的限制,它限制我們的控制流和資料流:結構化程式設計限制了控制權的直接轉移,面向對象程式設計限制了控制權的間接轉移,函數式程式設計限制了指派,相信你看到這裡一定一臉懵逼,啥叫控制權的直接轉移,啥叫控制權的間接轉移,不要着急,後邊詳細講解。

這三個程式設計範式最近的一個也有半個世紀的曆史了,半個世紀以來沒有提出新的程式設計範式,以後可能也不會了。因為程式設計範式的意義在于限制,限制了控制權轉移限制了資料指派,其他也沒啥可限制的了。很有意思的是,這三個程式設計範式提出的時間順序可能與大家的直覺相反,從前到後的順序為:函數式程式設計(1936年)、面向對象程式設計(1966年)、結構化程式設計(1968年)。

1.結構化程式設計

結構化程式設計證明了人們可以用順序結構、分支結構、循環結構這三種結構構造出任何程式,并限制了 goto 的使用。遵守結構化程式設計,工程師就可以像數學家一樣對自己的程式進行推理證明,用代碼将一些已證明可用的結構串聯起來,隻要自行證明這些額外代碼是确定的,就可以推導出整個程式的正确性。

前面提到結構化程式設計對控制權的直接轉移進行了限制,其實就是限制了 goto 語句。什麼叫做控制權的直接轉移?就是函數調用或者 goto 語句,代碼在原來的流程裡不繼續執行了,轉而去執行别的代碼,并且你指明了執行什麼代碼。為什麼要限制 goto 語句?因為 goto 語句的一些用法會導緻某個子產品無法被遞歸拆分成更小的、可證明的單元。而采用分解法将大型問題拆分正是結構化程式設計的核心價值。

其實遵守結構化程式設計,工程師們也無法像數學家那樣證明自己的程式是正确的,隻能像實體學家一樣,說自己的程式暫時沒被證僞(沒被找到bug)。數學公式和實體公式的最大差別,就是數學公式可被證明,而實體公式無法被證明,隻要目前的實驗資料沒把它證僞,我們就認為它是正确的。程式也是一樣,所有的 test case 都通過了,沒發現問題,我們就認為這段程式是正确的。

2.面向對象程式設計

面向對象程式設計包括封裝、繼承和多态,從架構的角度,這裡隻關注多态。多态讓我們更友善、安全地通過函數調用的方式進行元件間通信,它也是依賴反轉(讓依賴與控制流方向相反)的基礎。

在非面向對象的程式設計語言中,我們如何在互相解耦的元件間實作函數調用?答案是函數指針。比如采用C語言編寫的作業系統中,定義了如下的結構體來解耦具體的IO裝置, IO 裝置的驅動程式隻需要把函數指針指到自己的實作就可以了。

struct FILE {
    void (*open)(char* name, int mode);
    void (*close)();
    int (*read)();
    void (*write)(char);
    void (*seek)(long index, int mode);
}           

這種通過函數指針進行元件間通信的方式非常脆弱,工程師必須嚴格按照約定初始化函數指針,并嚴格地按照約定來調用這些指針,隻要一個人沒有遵守約定,整個程式都會産生極其難以跟蹤和消除的 Bug。是以面向對象程式設計限制了函數指針的使用,通過接口-實作、抽象類-繼承等多态的方式來替代。

前面提到面向對象程式設計對控制權的間接轉移進行了限制,其實就是限制了函數指針的使用。什麼叫做控制權的間接轉移?就是代碼在原來的流程裡不繼續執行了,轉而去執行别的代碼,但具體執行了啥代碼你也不知道,你隻調了個函數指針或者接口。

3.函數式程式設計

函數式程式設計有很多種定義很多種特性,這裡從架構的角度,隻關注它的沒有副作用和不修改狀态。函數式程式設計中,函數要保持獨立,所有功能就是傳回一個新的值,沒有其他行為,尤其是不得修改外部變量的值。前面提到函數式程式設計對指派進行了限制,指的就是這個特性。

在架構領域所有的競争問題、死鎖問題、并發問題都是由可變變量導緻的。如果有足夠大的存儲量和計算量,應用程式可以用事件溯源的方式,用完全不可變的函數式程式設計,隻通過事務記錄從頭計算狀态,就避免了前面提到的幾個問題。目前要讓一個軟體系統完全沒有可變變量是不現實的,但是我們可以通過将需要修改狀态的部分和不需要修改的部分分隔成單獨的元件,在不需要修改狀态的元件中使用函數式程式設計,提高系統的穩定性和效率。

綜上,沒有結構化程式設計,程式就無法從一塊塊可證僞的邏輯搭建,沒有面向對象程式設計,跨越元件邊界會是一個非常麻煩而危險的過程,而函數式程式設計,讓元件更加高效而穩定。沒有程式設計範式,架構設計将無從談起。

四、設計原則

和程式設計範式相比,設計原則和架構的關系更加緊密,設計原則就是架構設計的指導思想,它指導我們如何将資料和函數組織成類,如何将類連結起來成為元件和程式。反向來說,架構的主要工作就是将軟體拆解為元件,設計原則指導我們如何拆解、拆解的粒度、元件間依賴的方向、元件解耦的方式等。

設計原則有很多,我們進行架構設計的主導原則是 OCP(開閉原則),在類和代碼的層級上有:SRP(單一職責原則)、LSP(裡氏替換原則)、ISP(接口隔離原則)、DIP(依賴反轉原則);在元件的層級上有:REP(複用、釋出等同原則)、 CCP(共同閉包原則)、CRP(共同複用原則),處理元件依賴問題的三原則:無依賴環原則、穩定依賴原則、穩定抽象原則。

1.OCP(開閉原則)

設計良好的軟體應該易于擴充,同時抗拒修改。這是我們進行架構設計的主導原則,其他的原則都為這條原則服務。

2.SRP(單一職責原則)

任何一個軟體子產品,都應該有且隻有一個被修改的原因,“被修改的原因“指系統的使用者或所有者,翻譯一下就是,任何子產品隻對一個使用者的價值負責。該原則指導我們如何拆分元件。

舉個例子,CTO 和 COO 都要統計員工的工時,目前他們要求的統計方式可能是相同的,我們複用一套代碼,這時 COO 說周末的工時統計要乘以二,按照這個需求修改完代碼,CTO 可能就要過來罵街了。當然這是個非常淺顯的例子,實際項目中也有很多代碼服務于多個價值主體,這帶來很大的探秘成本和修改風險。

另外當一份代碼有多個所有者時,就會産生代碼合并沖突的問題。

3.LSP(裡氏替換原則)

當用同一接口的不同實作互相替換時,系統的行為應該保持不變。該原則指導的是接口與其實作方式。

你一定很疑惑,實作了同一個接口,他們的行為也肯定是一緻的呀,還真不一定。假設認為矩形的系統行為是:面積=寬*高,讓正方形實作矩形的接口,在調用 setW 和 setH 時,正方形做的其實是同一個事情,設定它的邊長。這時下邊的單元測試用矩形能通過,用正方形就不行,實作同樣的接口,但是系統行為變了,這是違反 LSP 的經典案例。

Rectangle r = ...
r.setW(5);
r.setH(2);
assert(r.area() == 10);           

4.ISP(接口隔離原則)

不依賴任何不需要的方法、類或元件。該原則指導我們的接口設計。

當我們依賴一個接口但隻用到了其中的部分方法時,其實我們已經依賴了不需要的方法或類,當這些方法或類有變更時,會引起我們類的重新編譯,或者引起我們元件的重新部署,這些都是不必要的。是以我們最好定義個小接口,把用到的方法拆出來。

5.DIP(依賴反轉原則)

跨越組建邊界的依賴方向永遠與控制流的方向相反。該原則指導我們設計元件間依賴的方向。

依賴反轉原則是個可操作性非常強的原則,當你要修改元件間的依賴方向時,将需要進行元件間通信的類抽象為接口,接口放在邊界的哪邊,依賴就指向哪邊。

6.REP(複用、釋出等同原則)

軟體複用的最小粒度應等同于其釋出的最小粒度。直白地說,就是要複用一段代碼就把它抽成元件。該原則指導我們元件拆分的粒度。

7.CCP(共同閉包原則)

為了相同目的而同時修改的類,應該放在同一個元件中。CCP 原則是 SRP 原則在元件層面的描述。該原則指導我們元件拆分的粒度。

對大部分應用程式而言,可維護性的重要性遠遠大于可複用性,由同一個原因引起的代碼修改,最好在同一個元件中,如果分散在多個元件中,那麼開發、送出、部署的成本都會上升。

8.CRP(共同複用原則)

不要強迫一個元件依賴它不需要的東西。CRP 原則是 ISP 原則在元件層面的描述。該原則指導我們元件拆分的粒度。

相信你一定有這種經曆,內建了元件A,但元件A依賴了元件B、C。即使元件B、C 你完全用不到,也不得不內建進來。這是因為你隻用到了元件A的部分能力,元件A中額外的能力帶來了額外的依賴。如果遵循共同複用原則,你需要把A拆分,隻保留你要用的部分。

REP、CCP、CRP 三個原則之間存在彼此競争的關系,REP 和 CCP 是黏合性原則,它們會讓元件變得更大,而 CRP 原則是排除性原則,它會讓元件變小。遵守REP、CCP 而忽略 CRP ,就會依賴了太多沒有用到的元件和類,而這些元件或類的變動會導緻你自己的元件進行太多不必要的釋出;遵守 REP 、CRP 而忽略 CCP,因為元件拆分的太細了,一個需求變更可能要改n個元件,帶來的成本也是巨大的。

一文讀懂架構整潔之道(附知識脈絡圖)

優秀的架構師應該能在上述三角形張力區域中定位一個最适合目前研發團隊狀态的位置,例如在項目早期,CCP比REP更重要,随着項目的發展,這個最合适的位置也要不停調整。

9.無依賴環原則

健康的依賴應該是個有向無環圖(DAG),互相依賴的元件,實際上組成了一個大元件,這些元件要一起釋出、一起做單元測試。我們可以通過依賴反轉原則 DIP 來解除依賴環。

10.穩定依賴原則

依賴必須要指向更穩定的方向。

這裡元件的穩定性指的是它的變更成本,和它變更的頻繁度沒有直接的關聯(變更的頻繁程度與需求的穩定性更加相關)。影響元件的變更成本的因素有很多,比如元件的代碼量大小、複雜度、清晰度等等,最最重要的因素是依賴它的元件數量,讓元件難以修改的一個最直接的辦法就是讓很多其他元件依賴于它!

元件穩定性的定量化衡量名額是:不穩定性(I) = 出向依賴數量 / (入向依賴數量 + 出向依賴數量)。如果發現違反穩定依賴原則的地方,解決的辦法也是通過 DIP 來反轉依賴。

11.穩定抽象原則

一個元件的抽象化程度應該與其穩定性保持一緻。為了防止高階架構設計和高階政策難以修改,通常抽象出穩定的接口、抽象類為單獨的元件,讓具體實作的元件依賴于接口元件,這樣它的穩定性就不會影響它的擴充性。

元件抽象化程度的定量化描述是:抽象程度(A)= 元件中抽象類和接口的數量 / 元件中類的數量。

将不穩定性(I)作為橫軸,抽象程度(A)作為縱軸,那麼最穩定、隻包含抽象類和接口的元件應該位于左上角(0,1),最不穩定、隻包含具體實作類,沒有任何接口的元件應該位于右下角(1,0),他們連線就是主序列線,位于線上的元件,他們的穩定性和抽象程度相比對,是設計良好的元件。位于(0,0)周圍區域的元件,它們是非常穩定(注意這裡的穩定指的是變更成本)并且非常具體的元件,因為他們的抽象程度低,決定了他們經常改動的命運,但是又有許多其他元件依賴他們,改起來非常痛苦,是以這個區域叫做痛苦區。右上角區域的元件,沒有其他元件依賴他們,他們自身的抽象程度又很高,很有可能是陳年的老代碼,是以這個區域叫做無用區。

一文讀懂架構整潔之道(附知識脈絡圖)

另外,可以用點距離主序列線的距離 Z 來表示元件是否遵循穩定抽象原則,Z 越大表示元件越違背穩定依賴原則。

五、架構工作的基本方針

了解了程式設計範式和設計原則,接下來我們看看如何應用他們拆分元件、處理元件依賴群組件邊界。架構工作有兩個方針:

盡可能長時間地保留盡可能多的可選項。這裡的可選項指的是無關緊要的細節設計,比如具體選用哪個存儲方式、哪種資料庫,或者采用哪種 Web 架構。業務代碼要和這些可選項解耦,資料庫或者架構應該做到像插件一樣切換,業務層對這個切換的過程應該做到完全無感。

低層次解耦方式能解決的,不要用高層次解耦方式。元件之間的解耦方式後邊細講,這裡強調的是邊界處理越完善,開發和部署成本越高。是以不完全邊界能解決的,不要用完全邊界,低層次解耦能解決的,不要用高層次解耦。

六、元件的拆分

首先要給元件下個定義:元件是一組描述如何将輸入轉化為輸出的政策語句的集合,在同一個元件中,政策的變更原因、時間、層次相同。

從定義就可以看出,元件拆分需要在兩個次元進行:按層次拆分、按變更原因拆分。

這裡的變更原因就是業務用例,按變更原因進行元件拆分的例子是:訂單元件、聊天元件。按層次拆分,可以拆為:業務實體、用例、接口擴充卡、架構與驅動程式。

  • 業務實體:關鍵業務資料和業務邏輯的集合,與界面無關、與存儲無關、與架構無關,隻有業務邏輯。
  • 用例:特定場景下的業務邏輯,可以了解為 輸入 + 業務實體 + 輸出 = 用例。
  • 接口擴充卡:包含整個整個MVC,以及對存儲、裝置、界面等的接口聲明和使用。

一條政策距離系統的輸入、輸出越遠,它的層次越高,是以業務實體是最高的層,架構與驅動程式是最低的層。

七、元件依賴處理

前面拆好了元件分好了層,依賴就很好處理了:依賴關系與資料流控制流脫鈎,而與元件所在層次挂鈎,始終從低層次指向高層次,如下圖。越具體的政策處在的層級越低,越插件化。切換資料庫是架構驅動層的事情,接口擴充卡完全無感覺,切換展示器是接口擴充卡層面的事情,用例完全無感覺,而切換用例也不會影響到業務實體。

一文讀懂架構整潔之道(附知識脈絡圖)

八、元件邊界處理

一個完整的元件邊界包括哪些内容?首先跨越元件邊界進行通信的兩個類都要抽象為接口,另外需要聲明專用的輸入資料模型、聲明專用的傳回資料模型,想一想每次進行通信時都要進行的資料模型轉換,就能了解維護一個元件邊界的成本有多高。

除非必要,我們應該盡量使用不完全邊界來降低維護元件邊界的成本。不完全邊界有三種方式:

  • 省掉最後一步:聲明好接口,做好分割後,仍然放在一個元件中,等到時機成熟時再拆出來獨立編譯部署。
  • 單向邊界:正常的邊際至少有兩個接口,分别抽象調用方和被調用方。這裡隻定義一個接口,高層次元件用接口調用低層次元件,而低層次元件直接引用高層次元件的類。
  • 門戶模式:控制權的間接轉移不用接口和實作去做,而是用門戶類去做,用這種方式連接配接口都不用聲明了。

除了完全邊界和不完全邊界的區分,邊界的解耦方式也可以分為3個層次:

  • 源碼層次:做了接口、類依賴上的解耦,但是放在同一個元件中,通常放在不同的路徑下。和不完全邊界的省略最後一步一樣。
  • 部署層次:拆分為可以獨立部署的不同元件,比如 iOS 的靜态庫、動态庫,真正運作時處于同一台實體機器上,元件之間通常通過函數調用通訊。
  • 服務層次:運作在不同的機器上,通過 url 、網絡資料包等方式進行通訊。

從上到下,(開發、部署)成本依次升高,如果低層次的解耦已經滿足需要,不要進行高層次的解耦。

本文作者:

韓帥,阿裡巴巴進階無線開發工程師,關注架構、IM、應用包瘦身等領域。

繼續閱讀