架構是研究“分”和“合”的藝術,通過“分離關注點”將系統拆分為多個部分,然后在“原則和規則”的約束下對組件進行裝配,形成高內聚的構件;再根據需求對多個構件進行關聯,形成低耦合的連接,最終構建“高內聚低耦合”的軟件系統。
image
為了有效應對軟件復雜性,通常會對其進行分類,然后對癥下藥逐個擊破。
1. 軟件系統復雜性
面對一個軟件需求,我們經常會將其分為兩類:
- 功能性需求。就是產品提出的眾多業務功能,例如:用戶登錄、查詢數據、添加訂單等;
- 非功能性需求。指系統在實現功能時必須滿足的技術指標,最常見的包括性能、可靠性、安全性、可維護性、易用性等,例如:系統的響應時間、并發訪問量、容錯能力、數據安全性、可擴展性等。
其實,這兩類需求整好與軟件系統的兩類“復雜性”一一對應:
- 業務復雜性,指系統中業務邏輯和業務規則的復雜程度。業務復雜性主要來自于業務的規模、結構、變化性等,這個與軟件所在領域有極大關系;
- 技術復雜性,指系統中所用技術的復雜程度。技術復雜性主要來自于所使用的數據庫、網絡協議、中間件、應用框架等,這與軟件架構和基礎設施關系巨大;
面對不同的需求和復雜性,其設計目標和應對方式也完全不同。
1.1. 業務復雜性
首先,看下業務復雜性:
image
對于業務需求,我們追求的是系統的復用性和擴展性。
- 復用性。指組件或模塊能夠在不同的場景下被重復使用。高復用性設計能夠大幅度減少開發和維護的成本,提高開發效率。組件的粒度越小復用性越高,功能結構越清晰,邏輯調整越便利,與之相反的是代碼重復率,重復代碼是系統腐化的重要標志;
- 擴展性。能夠在不改變現有系統結構的情況下,方便地添加新的功能或修改現有功能。具有高擴展性的系統能夠滿足未來需求的變化,降低系統的維護成本。
導致業務代碼變化的原因有很多,比如:
- 線上bug。線上bug很少但對業務系統的傷害巨大,發現bug后為了快速修復問題,往往選擇短平快的方式而非最佳方案,這些“補丁”就像系統中的“飛線”在代碼中穿梭,成為超出三界的定時炸彈,一不小心就會給你意外的“驚喜”;
- 新功能需求。這是代碼膨脹的主要推動力,開發人員將產品提出的需求翻譯成代碼,不停的“塞入”到代碼倉庫,導致倉庫快速膨脹,很快你將面對幾十萬行代碼并在之上進行新的開發;
- 創新性業務。意味著新建系統、新建倉庫、新建服務,看起來一切非常良好,可以一次性甩掉多年的歷史包袱,但公司整個系統變得越來越復雜,甚至沒人能說出服務間的調用關系;
這些變更都會導致業務代碼越來越多、邏輯越來越復雜,最終變得難以維護。
為了更好的應對這些變化,常見的手段包括:
- DDD, 構建于“領域模型”基礎之上,使用面向對象的各種語言特性,實現邏輯的封裝、復用;將業務概念和實現組件結合在一起,避免相互轉化,從而降低溝通成本;
- 重構,隨著業務的變化,對原有代碼結構進行優化,在新結構上以擴展的方式完成新功能的添加,不斷地對代碼結構進行調優
- TDD,重構的重要保障,在優化代碼結構的同時,保障不會破壞原有的業務邏輯
業務復雜性先簡單介紹到這,接下來看下技術復雜性:
1.2. 技術復雜性
image
對于非功能需求,我們追求的是系統的高性能和高可用。
- 高性能。在同等資源下,要么讓系統運行盡可能快,要么讓系統吞吐盡可能大
- 高可用。盡量保障系統 7 * 24h 不間斷的提供服務,避免由于服務中斷導致公司損失
導致技術復雜性激增的原因有很多,比如:
- 用戶和并發量。隨著用戶和并發量的激增,系統承受的壓力將越來越大,一個小小的卡點便能造成巨大的損失
- 系統數據量。隨著系統數據量不斷積累,當單表數據量超過 億 級,不管是訪問速度還是異常恢復都將面臨巨大挑戰
- 機器規模。當機器規模超過一定的閾值,硬件問題將頻繁爆發,基本每天都會出現硬件故障,最終成為高可用的阻力
這些問題并不能通過簡單的增加代碼來解決,往往需要引入新技術或使用新方案:
- 新技術。當數據庫表數據量達到“億”級出現明顯的性能瓶頸時,可以應用 分庫分表 或 TiDB 來解決
- 新方案。當數據庫讀壓力巨大,可以調整技術方案,使用數據庫讀寫分離、增加緩存等方案解決
通過對比,是否有一種感覺:“業務”與“技術” 差距巨大,可以說是天壤之別。所以需要一種架構,能夠對 “業務” 和 “技術” 進行隔離,降低兩者的相互影響,使得:
- 技術調整不影響業務模型
- 業務調整不能依賴技術
六邊形架構,便可以解決這個問題。
2. 六邊形架構
六邊形架構出自《實現領域驅動設計》一書,與“洋蔥架構” 非常相似,都能很好的實現 “業務” 與 “技術” 的分離。
在 “Command側”(詳見CQRS架構) DDD 仍舊是最佳解決方案,所以在此使用 “六邊形架構” 作為頂級架構。
六邊形架構如下:
image
該架構由內外兩個六邊形組成,這也是其名稱來源:
- 內六邊形屬于業務域,用于應對業務復雜性。
- 外六邊形屬于技術域,用于應對技術復雜性。
- 其中內六邊形是整個系統的核心,外六邊形依賴于內六邊形,而內六邊形不依賴于外六邊形
- 內外兩個六邊形存在清晰的邊界,兩者相互獨立,互不影響,獨自演進
2.1.【業務】內六邊形
內六邊形聚焦于業務邏輯,使用良好的設計應對業務的復雜性;通過提升系統的復用性和擴展性,來應對未來的變化。
2.1.1. DDD
DDD 是解決復雜業務的一把利器,將業務需求和代碼實現相結合,通過對業務領域的深入理解,構建出可復用、可維護、可擴展的領域模型。
DDD 分為戰略和戰術兩部分,在此重點闡述戰術部分。戰術模型主要包括:
- 實體(Entity):具有唯一標識的領域對象,具有豐富的屬性和行為,表示需要持續跟蹤的領域概念,比如 用戶、訂單、地址等;
- 值對象(Value Object):沒有唯一標識的領域對象,具有屬性和行為,一般用于表示某一個領域概念,比如 金額、郵箱、手機號等;
- 聚合根(Aggregate Root):一組由實體和值對象組成的高內聚對象集合,由一個根實體來管理它們,我們也稱之為聚合根,比如 訂單+訂單項便組成了一個聚合,其中訂單為聚合根;
- 工廠(Factory):創建領域對象的一種機制,也是設計模式在 DDD 中的落地,它隱藏了對象創建細節,并保障創建對象的有效性,主要解決復雜對象初始化問題;
- 存儲庫(Repository):提供對領域對象的持久化和檢索功能,將領域對象從數據存儲細節中分離出來,完成領域對象和存儲引擎的解耦;
- 領域服務(Service):處理領域對象之間的交互,沒有自己的狀態,封裝了一些業務邏輯,主要用于需要多個領域對象相互協作才能完成的業務場景,比如銀行轉賬;
- 領域事件(Event):指在領域內發生的、有意義的、需要被捕捉的事件,主要完成服務內的流程解耦和服務間的系統解耦;
這些領域對象相互協作,共同承載復雜的業務場景,大幅提升了業務的復用性和擴展性。
2.1.2. TDD
也稱為測試驅動開發,它的基本思想是在編寫代碼之前先編寫測試,然后根據測試來編寫代碼,最后再運行測試。可以幫助開發人員更快地發現并修復代碼中的bug,還可以讓開發人員更加關注代碼的設計,使代碼更加健壯、可擴展和易維護。
業務模型與基礎設施的解耦將為你的 TDD 帶來眾多好處:
- 提高測試的速度:使用 Mock 技術可以避免測試過程中涉及到緩慢的網絡或數據庫操作,從而提高測試速度,加快迭代周期;
- 提高測試可靠性:使用 Mock數據可以降低測試失敗或不可靠的情況,避免由于數據變更所造成的測試不穩定問題;
- 更好的測試隔離:業務邏輯和基礎設施可以并行開發,避免兩者相互干擾而產生不確定行為;
- 有利于重構:業務邏輯和外部依賴分離,在重構代碼時,可以聚焦于業務邏輯而不必擔心外部依賴的穩定性和正確性;
2.1.3. 兩頂帽子
兩頂帽子,是落地重構的重要開發模式,是一種工作習慣,或者說是一種高效的工作流程。
兩頂帽子法,將軟件變更落地過程分為兩個階段:
- 優化結構階段。在不改變軟件行為的前提下,對軟件結構進行優化,使其更具擴展性。比如:
- 【重構】抽取公共邏輯到方法、類,以便更好的被復用;
- 【設計模式】抽取模板方法,對核心邏輯進行統一;
- 【架構模式】抽取功能微內核,確定插件簽名和功能;
- 添加功能階段。在調整后的具備更好的擴展性的代碼基礎上完成功能調整或者增加新功能,如:
- 新功能復用已有組件能力,避免重復開發;
- 以擴展點的方式增加新功能,提升開發效率;
2.2.【技術】外六邊形
外六邊形聚焦于技術,與公司基礎設施密切相關,也受所用框架的各種約束。其核心是發揮框架和中間件的優勢,更好的為業務提供服務。
根據應用場景的不同,我們將外六邊形的組件分成兩類:
- 輸入適配器。將來自外部的數據轉換為系統可以使用的格式;
- 輸出適配器。將系統內部的數據轉換為外部可以使用的格式
他們是系統與外部的“翻譯官”,將外部請求轉換為系統可以理解的指令,并將系統響應轉換為外部世界可以理解的格式,這種松耦合可以使系統更加靈活更具擴展性。
2.2.1. 輸入適配器
負責將外部系統的請求轉換為應用服務能夠處理的格式,通常包括Web 請求、RPC調用、消息隊列、定時任務等,是將外部請求轉換為內部指令的橋梁。
常見的輸入適配器主要包括:
- Web 請求。處理 HTTP 請求,對請求進行轉換、驗證,將其轉換為應用服務所需格式,調用接口完成業務邏輯,最后將處理結果轉化為所需格式進行返回。常見的框架有 Spring MVC、Struct2等;
- RPC 調用。處理遠程調用請求,將請求轉換為應用程序所需格式,調用應用服務接口完成業務邏輯,最后返回處理結果;在 Spring Cloud 技術棧下,與 Web 請求高度類似,但仍舊具有自己的特點,需要與 Web 請求進行區分處理。常見的 RPC 框架有 Spring Cloud、gRPC、Thrift、Dubbo等;
- 消息隊列。主要指的是消息隊列的消費端,從消息隊列中讀取消息,將信息轉換為應用程序所需格式,調用應用服務接口完成業務邏輯。常見的有 RocketMQ、Kafka、RabbitMQ等;
- 定時任務。由定時器周期性觸發,調用應用服務的業務方法,完成某種后臺任務。常見的有Quartz、XXL-job、Spring Task 等;
2.2.2. 輸出適配器
負責將應用程序輸出結果轉換為外部系統能夠理解的格式,通常包括數據庫、RPC調用、緩存、搜索、消息隊列、文件系統等,是將內部響應轉換為外部響應的橋梁。
常見的輸出適配器主要包括:
- 數據庫。將領域模型中的模型數據保存到數據庫進行持久化存儲,常用的框架包括 MyBatis、Jpa、Hibernate等,中間件主要是 MySQL;
- 緩存。模型數據發生變更后,對緩存數據進行清理或更新,常見框架包括本地緩存 Guava、Caffeine、EhCache,分布式緩存有 redis、Memcache、TAIr等;
- 搜索。為應對多維度查詢,系統會引入搜索引擎組件,在模型數據發生變更后,需要將變更同步到搜索引擎,常見的有Elasticsearch、Solr、Sphinx等;
- 消息隊列。這里主要指的是消息隊列的發送端,當業務操作完成后,系統會向外發布領域事件,以將變更通知到下游系統,常見的有 RocketMQ、Kafka、RabbitMQ等;
- RPC調用。這里主要指的是 RPC 的調用端,當業務模型對其他領域服務存在依賴時,需要通過 RPC 進行系統通信,常見的 RPC 框架有 Spring Cloud、gRPC、Thrift、Dubbo等;
- 文件系統。這里簡單理解為系統的日志輸出即可,常見的有Log4j、Logback、SLF4J、JUL等;
這么多紛繁復雜的框架、中間件從另一個層面也驗證了,外六邊形是以技術作為驅動的,其核心就是:如何更好的使用這些技術工具,發揮每個框架的強項。
3. 小結
任意一個業務系統都會面對兩類需求:
- 來自業務的功能性需求;
- 來自技術的非功能性需求;
兩類需求背后的驅動力(復雜性)完全不同:
- 功能性需求的驅動力在于業務自身的復雜性和多變性;
- 非功能性需求的驅動力在于架構的變更和技術的更迭;
為了更好的應對這兩類變化,需要在架構上進行隔離,避免相互影響帶來更多的復雜性,然后逐個擊破。這就是六邊形架構最擅長的領域:
- 內六邊形。聚焦于業務解決功能性需求,常用的手段有 DDD、TDD、重構;
- 外六邊形。聚焦于技術解決非功能性需求,需要使用好 輸入適配器 和 輸出適配器;