程序的世界飛速發展,今天所掌握的技能可能明年就過期了,但有一些東西是歷久彌新,永遠不變的,掌握了這些,在程序的海洋裏就不會迷路,架構思想就是這樣一種東西。數據庫
本文是《架構整潔之道》的讀書筆記,文章從軟件系統的價值出發,認識架構工做的價值和目標, 依次瞭解架構設計的基礎、指導思想(設計原則)、組件拆分的方法和粒度、組件之間依賴設計、組件邊界多種解耦方式以及取捨、下降組件之間通訊成本的方法,從而在作出正確的架構決策和架構設計方面,給出做者本身的解讀。編程
阿里巴巴中間件微信公衆號對話框,發送「架構」,可獲取《架構整潔之道》知識脈絡圖。數組
架構是軟件系統的一部分,因此要明白架構的價值,首先要明確軟件系統的價值。軟件系統的價值有兩方面,行爲價值和架構價值。安全
行爲價值是軟件的核心價值,包括需求的實現,以及可用性保障(功能性 bug 、性能、穩定性)。這幾乎佔據了咱們90%的工做內容,支撐業務先贏是咱們工程師的首要責任。若是業務是明確的、穩定的,架構的價值就能夠忽略不計,但業務一般是不明確的、飛速發展的,這時架構就無比重要,由於架構的價值就是讓咱們的軟件(Software)更軟(Soft)。能夠從兩方面理解:微信
當咱們只關注行爲價值,不關注架構價值時,會發生什麼事情?這是書中記錄的一個真實案例,隨着版本迭代,工程師團隊的規模持續增加,但總代碼行數卻趨於穩定,相對應的,每行代碼的變動成本升高、工程師的生產效率下降。從老闆的視角,就是公司的成本增加迅猛,若是營收跟不上就要開始賠錢啦。網絡
可見架構價值重要性,接下來從著名的緊急重要矩陣出發,看咱們如何處理好行爲價值和架構價值的關係。閉包
重要緊急矩陣中,作事的順序是這樣的: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 框架。業務代碼要和這些可選項解耦,數據庫或者框架應該作到像插件同樣切換,業務層對這個切換的過程應該作到徹底無感。
低層次解耦方式能解決的,不要用高層次解耦方式。組件之間的解耦方式後邊細講,這裏強調的是邊界處理越完善,開發和部署成本越高。因此不徹底邊界能解決的,不要用徹底邊界,低層次解耦能解決的,不要用高層次解耦。
首先要給組件下個定義:組件是一組描述如何將輸入轉化爲輸出的策略語句的集合,在同一個組件中,策略的變動緣由、時間、層次相同。
從定義就能夠看出,組件拆分須要在兩個維度進行:按層次拆分、按變動緣由拆分。
這裏的變動緣由就是業務用例,按變動緣由進行組件拆分的例子是:訂單組件、聊天組件。按層次拆分,能夠拆爲:業務實體、用例、接口適配器、框架與驅動程序。
一條策略距離系統的輸入、輸出越遠,它的層次越高,因此業務實體是最高的層,框架與驅動程序是最低的層。
前面拆好了組件分好了層,依賴就很好處理了:依賴關係與數據流控制流脫鉤,而與組件所在層次掛鉤,始終從低層次指向高層次,以下圖。越具體的策略處在的層級越低,越插件化。切換數據庫是框架驅動層的事情,接口適配器徹底無感知,切換展現器是接口適配器層面的事情,用例徹底無感知,而切換用例也不會影響到業務實體。
一個完整的組件邊界包括哪些內容?首先跨越組件邊界進行通訊的兩個類都要抽象爲接口,另外須要聲明專用的輸入數據模型、聲明專用的返回數據模型,想想每次進行通訊時都要進行的數據模型轉換,就能理解維護一個組件邊界的成本有多高。
除非必要,咱們應該儘可能使用不徹底邊界來下降維護組件邊界的成本。不徹底邊界有三種方式:
除了徹底邊界和不徹底邊界的區分,邊界的解耦方式也能夠分爲3個層次:
從上到下,(開發、部署)成本依次升高,若是低層次的解耦已經知足須要,不要進行高層次的解耦。
原文連接 本文爲雲棲社區原創內容,未經容許不得轉載。