正如我們之前提到的,一個良好的架構必須支持以下幾個方面:
- 系統的使用案例
- 系統的運行
- 系統的維護
- 系統的開發以及部署。
USE CASES 用例
系統的架構必須能夠支持其設計的目的。
以購物車應用為例,其架構應直接支持購物車的所有可能用例。
架構師首要關注的是確保架構能支撐這些用例。儘管架構對系統的行為影響不大,但良好的架構能夠清晰地展示和解釋這些行為,使得系統的意圖在架構層面上可見。
這樣的架構讓開發人員能夠容易地辨識系統行為,不需深入尋找。行為會透過類、函數或模塊等主要元素在架構中明顯顯示,並具有清楚描述其功能的名稱。
第21章「尖叫的架構」將對此進行更詳細的闡述。
OPERATION 運行
架構在支持系統運作方面扮演著實際而非僅是外觀的角色。例如,若系統每秒需處理100,000個用戶,則其架構必須支持這種吞吐量和對應用例所需的響應時間。同樣地,如果系統需要在毫秒級別快速查詢大數據庫,架構也應該支持這種操作。
針對不同的系統需求,架構的支持形式也會有所不同。對某些系統而言,這可能意味著將處理元素組織成一系列可在多個服務器上平行運行的小型服務。對其他系統來說,則可能採用許多輕量級線程共享單一處理器上單一進程的地址空間。還有一些系統可能僅需要幾個在獨立地址空間中運行的進程。甚至有些系統可以作為在單一進程中運行的簡單單體程序繼續存在。
儘管可能看似奇怪,這種決策也是優秀架構師應留有彈性的選擇之一。一個基於單體結構開發的系統,對該結構有依賴,將其改造為多進程、多線程或微服務可能並不容易。相較之下,一個在組件間進行適當隔離、且不預設組件間通信方式的架構,將更容易隨著系統運行需求的變化,過渡到不同的線程、進程或服務模式。
DEVELOPMENT 開發
在支持開發環境方面,系統架構扮演著重要角色,這時康威定律就顯得尤為重要。康威定律指出,任何設計系統的組織,其系統設計的結構往往會模仿該組織的溝通結構。這意味著組織內部的溝通方式將直接影響到系統架構的形式。
對於需要由多個團隊合作開發的系統,其架構必須促進團隊間的獨立操作,以避免開發過程中相互干擾。這可以通過將系統適當地劃分成隔離良好、可獨立開發的組件來實現。這些組件隨後可以分配給可以獨立工作的各個團隊,從而保證各隊的獨立性和效率。
DEPLOYMENT 部署
系統架構在決定系統部署的便捷性方面扮演著關鍵角色。目標是實現“立即部署”。一個優良的架構不應依賴於大量配置腳本和屬性文件的調整,也不需要手動創建必須精確配置的目錄或文件。一個好的架構應當支援在構建後即刻部署系統。
實現這一點,需通過適當的系統組件切分和隔離,包括那些將整個系統緊密聯合起來的主要組件。這些組件確保了系統中每個部分的正確啟動、整合與監控,從而支援系統的無縫部署。
LEAVING OPTIONS OPEN 保留可選項
一個優良的架構應該能夠有效地權衡所有先前提到的關注點,並構建一個能夠滿足所有這些需求的組件結構。這聽起來似乎很簡單,但實際上僅是論述上的簡單。
實際上,達成這種平衡相當困難。主要的挑戰是,我們大多數時間都無法完全預知所有的使用案例、運行限制、團隊結構或部署需求。更糟的是,即使我們能知道這些信息,隨著系統生命周期的發展,這些需求也會不可避免地發生變化。總的來說,我們需要達成的目標本身是不明確且變化無常的。
然而,情況並非毫無希望:一些實現成本較低的架構原則可以幫助我們在還沒有明確目標的情況下平衡這些擔憂。這些原則有助於我們將系統劃分為互相隔離良好的組件,使我們能夠在可能的情況下,盡可能長時間地保持多種選擇。
一個好的架構使系統容易進行必要的改變,通過保留選擇的彈性,應對所有必須進行的變更。
DECOUPLING LAYERS 按層解藕
從用例的角度來看,架構師的目標是讓系統結構支持所有必需的用例,但往往無法預知所有的用例。然而,架構師通常知道系統的基本設計意圖,例如是設計購物車系統、運輸清單系統或訂單處理系統。因此,架構師可以運用單一責任原則(SRP)和共同封閉原則(CCP)來隔離因不同原因變更的部分,並集成因相同原因變更的部分。
哪些部分因不同原因變更?在某些情況下這很明顯。例如,用戶界面的變更與業務規則無關,而業務用例則涵蓋這兩者。因此,一位優秀的架構師會將用例的用戶界面部分與業務規則部分分開,使它們可以獨立變更,同時保持用例的清晰和完整性。
業務規則本身可能與應用程序緊密相連,也可能更加通用。例如,輸入欄位的驗證是與應用程序本身緊密相連的業務規則。相反,計算帳戶利息和庫存統計是與特定領域更緊密相關的業務規則。這兩種不同類型的規則變更的速度和原因不同,因此應該將它們分開,以便可以獨立變更。
至於數據庫及其查詢語言,甚至是數據架構,這些都是與業務規則或用戶界面無關的技術細節。這些細節的變更速度和原因獨立於系統的其他方面。因此,架構應該將這些技術細節與系統的其他部分隔離,以便可以獨立進行變更。
通過這種方式,我們可以看到系統被劃分為多個解耦的水平層次——用戶界面、應用特定的業務規則、應用獨立的業務規則,以及數據庫等,僅舉幾例。
DECOUPLING USE CASES 用例的解耦
首先,用例本身也是因不同的原因而變更。例如,添加新訂單的用例與刪除訂單的用例在變更的原因和速率上幾乎肯定是不同的。因此,按照用例來劃分系統是一個非常自然的選擇。
同時,這些用例構成了貫穿系統水平分層的垂直切面。每個用例都會涉及一些用戶界面(UI)、特定應用的業務規則、應用無關的業務規則以及數據庫功能。所以,在我們將系統劃分為水平層的同時,也在按用例將其劃分為穿透這些層的狹窄垂直用例。
為了達到這種解耦,我們需要將添加訂單用例的用戶界面與刪除訂單用例的用戶界面分開處理。同樣的處理方式也適用於業務規則和數據庫,我們應保持用例在系統的垂直方向上保持分離。
從這些設計中,我們可以看到一個模式:如果我們根據不同的變更原因對系統元素進行解耦,那麼我們就可以在不干擾現有用例的情況下持續添加新的用例。如果我們還將支持這些用例的用戶界面和數據庫分組,使得每個用例使用UI和數據庫的不同方面,那麼添加新的用例就不太可能影響到舊有的用例。
DECOUPLING MODE 解耦的模式
首先,考慮這些解耦對系統運行的影響。如果不同方面的用例得到良好隔離,那麼需求高吞吐量的用例自然與需求低吞吐量的用例分開。此外,如果用戶界面和數據庫從業務規則中分離,它們便可以在不同的伺服器上運行。對於那些需要較高帶寬的應用,可以在多個伺服器上進行複製。
總之,為了用例而進行的解耦也有助於改善系統的運行效率。然而,要從運營上獲得好處,解耦必須採用適當的方式。例如,為了能在不同伺服器上運行,分離的組件不能依賴於同一處理器的同一地址空間,它們必須是獨立的服務,通過某種網絡進行通訊。
許多架構師將這些組件稱為“服務”或“微服務”,具體稱呼往往依賴於程式碼行數的模糊概念。基於服務的架構常被稱為面向服務的架構(Service-Oriented Architecture, SOA)。這裡提到的SOA或微服務並非是提倡它們就是最佳架構或未來趨勢,而是強調有時我們需要將組件分隔到服務層次。
請記住,優秀的架構會保持彈性選擇。解耦模式就是這些選擇之一。在深入探討這個話題之前,我們會回顧其他兩個設計目標。
INDEPENDENT DEVELOP-ABILITY 開發的獨立性
在架構設計的第三個目標——支持系統開發方面,當系統組件之間實現高度解耦後,開發團隊間的干擾會大幅減少。例如,如果業務規則與用戶界面(UI)無關,那麼專注於UI開發的團隊就不太可能對專注於業務規則的團隊造成影響。同理,如果各個用例之間也實現了解耦,專注於添加訂單(addOrder)用例的團隊不大可能干擾到處理刪除訂單(deleteOrder)用例的團隊。
只要系統的層與用例進行了適當的解耦,無論團隊是按功能、按組件、按層次還是其他任何方式組織,系統的架構都能支持多團隊合作的組織架構。這樣的解耦不僅提高了開發效率,也增加了各團隊之間的協作彈性。
INDEPENDENT DEPLOYABILITY 部屬的獨立性
這種按用例和水平分層的解耦同樣為系統的部署帶來了極高的靈活性。實際上,如果解耦工作做得很好,我們甚至可以在系統運行中進行各個分層實現和具體用例的熱切換。在這種情況下,增加新的用例可能僅僅需要在系統中添加一些新的jar文件或啟動一些服務,而不會影響到系統的其他部分。
DUPLICATION 重複
架構師常常會陷入一種困境,那就是害怕重複。在軟件開發中,重複通常被認為是壞事,我們不喜歡代碼的重複。當真正的代碼重複出現時,我們作為專業人士有責任去減少並消除這種重複。
然而,重複有不同的類型。真正的重複指的是當一個實例發生變更時,必須在該實例的所有副本上做出同樣的變更。還有一種是假的或偶然的重複,如果看似重複的代碼部分隨著時間沿著不同的路徑演進——它們以不同的速率變更,出於不同的原因——那麼它們就不是真正的重複。幾年後回顧,你會發現它們已經大不相同。
想像有兩個用例在屏幕結構上非常相似,架構師可能會強烈傾向於共用那些結構的代碼。但他們真的應該這麼做嗎?這是真正的重複還是偶然的重複?大多數情況下,這種重複很可能是偶然的。隨著時間的推移,這兩個屏幕可能會逐漸演變,最終變得完全不同。因此,應該小心避免將它們統一起來,否則未來分離它們將會是一個挑戰。
當我們按用例垂直切分系統時,這樣的問題會經常出現,並且我們常常會被誘惑去耦合這些用例,因為它們具有相似的屏幕結構、相似的算法或相似的數據庫查詢和/或架構。一定要小心,抵抗因應激性反應而消除重複的誘惑。確保這種消除僅針對真正的重複。
同樣,當我們對系統進行水平分層時,也可能會發現某個數據庫記錄的數據結構與某個屏幕視圖的數據結構非常相似。你可能會被誘惑,直接將數據庫記錄上傳到用戶界面,而不是創建一個看似相同的視圖模型並跨模型複製元素。請小心:這種重複幾乎肯定是偶然的。創建獨立的視圖模型並不會花費太多努力,並且它將幫助你保持層之間適當的解耦。
DECOUPLING MODES (AGAIN) 再談解耦模式
讓我們回顧解耦模式的問題。系統可以通過多種方式進行按水平分層和用例解耦,包括在源碼層次、二進制代碼(部署)層次以及執行單元(服務)層次。
在源碼層次解耦中,我們可以控制源代碼模組之間的依賴,從而使得對一個模組的變更不會強迫對其他模組進行改變或重新編譯。在這種模式下,所有組件都在同一個地址空間內執行,並通過簡單的函數調用彼此交互,系統以單一可執行文件的形式載入到計算機內存中。
部署層次解耦則涉及控制可部署單元(如Jar文件、DLL或共享庫)之間的依賴,使得單一模組的源碼變更不會引起其他模組的重建和重新部署。這些組件可能仍然位於同一地址空間中,並通過函數調用進行通訊。而一些其他組件可能存在於同一處理器下的不同進程中,並通過進程間通訊、套接字或共享內存進行通信。
服務層次解耦則將組件間的依賴降到數據結構層面,僅通過網絡數據包進行通信,使得每個執行單元對其他單元的源碼和二進制變更完全獨立。
選擇哪種模式最合適,取決於項目的早期階段難以確定。隨著項目的成熟,最佳模式可能會發生變化。例如,一個目前在單一服務器上運行良好的系統可能最終需要一些組件在不同的服務器上運行。
流行的解決方案之一是默認在服務層次進行解耦。這種方法成本較高,並且傾向於粗粒度解耦。即使微服務的粒度很小,解耦也可能不夠精細。
服務層次解耦不僅開發時間和系統資源成本高昂,處理不必要的服務邊界也是一種努力、記憶和週期的浪費。記憶和週期雖便宜,但人力成本始終較高。
我的偏好是將解耦推進到可以形成服務的地步,但只要可能,就讓組件保持在同一地址空間內。這樣做留給未來轉變為服務的選項。
這種方法起初在源碼層次對組件進行分離,這對項目的整個壽命可能已經足夠。然而,如果出現部署或開發問題,將一些解耦推進到部署層次可能就足夠了。
隨著開發、部署和運營問題的增加,我會仔細選擇哪些可部署單元轉化為服務,並逐步將系統向該方向轉移。隨著時間的推移,系統的運維需求可能會下降,曾經需要在服務層次進行解耦的系統現在可能只需在部署層次甚至是源碼層次解耦。
一個良好的架構應允許系統以單體結構出生,以單一文件部署,然後逐漸成長為一組相互獨立的可部署單元,最終發展到獨立的服務或微服務。後來,隨著情況變化,應允許系統逐步回退到單體結構。良好的架構還應保護大部分源碼免受這些變更的影響,將解耦模式保持為一個選項,使得大規模部署可以使用一種模式,小規模部署則可以使用另一種模式。
CONCLUSION 本章小結
要達成先前討論的解耦要求確實具有一定難度。我並未暗示系統的解耦模式應該只是一個簡單的配置選項(雖然在某些情況下這是適當的)。我想表達的是,系統的解耦模式很可能隨著時間的推移而變化,一位優秀的架構師應該能夠預見這些變化並適當地促進這些變化的發生。