DDD专家张逸:复杂与架构演进的关系
圖
張逸
讀完需要
25
分鐘速讀僅需 5 分鐘
張逸,架構編碼實踐者,IT 文藝工作者,大數據平臺架構師,兼愛面向對象與函數式程序設計,熱衷于編程語言學習與技藝提升,致力于將主流領域驅動設計(DDD)與函數式編程、響應式編程以及微服務架構完美結合。個人微信公眾號:逸言。個人博客: ?(http://iamzhangyi.github.io)
復雜的事物讓人著迷,繁復、多樣、無序以及其中蘊含的無窮變化或許也是我覺得軟件設計有趣的地方。由于設計的復雜性,我在每次面臨不同的項目、不同的產品時,油然而生一種耳目一新的感覺,似乎重啟了新的旅程,風景不同,心境自然也就不同了。
然而,復雜并不總是令人感到有趣,除非我們具有掌控復雜的能力。
那么,什么是復雜?
1
? ?
什么是復雜
Jurgen Appelo 在分析復雜系統理論時,將 Complicated 與 Complex 分別放在理解力與預測能力兩個迥然不同的維度上。Complicated 與 Simple(簡單)相對,意指非常難以理解, 而 Complex 則介于 Ordered(有序的)與 Chaotic(混沌的)之間,意指在某種程度上可以預測,但會有很多出乎意料的事情發生,如圖 4.1 所示。
大多數軟件系統是難以理解的,雖然我們可以遵循一些設計原則來應對未來的變化,但由于未來是不可預測的,因而軟件的演進其實存在不可預測的風險。如此看來,軟件系統所謂的“復雜”其實覆蓋了 Complicated 與 Complex 兩個方面,等同于圖 4.1 中城市所處的位置。湊巧的是,Sam Newman 也認為城市的變遷與軟件的演化存在很大程度的相似性:
圖 4.1
很多人把城市比作生物,因為城市會時不時地發生變化。當居民對城市的使用方式有所變化,或者受到外力的影響時,城市就會相應地演化。
上面描述的城市和軟件的對應關系應該是很明顯的。當用戶對軟件提出變更需求時,我們需要對其進行響應并做出相應的改變。未來的變化很難預見,所以與其對所有變化的可能性進行預測,不如做一個允許變化的計劃。
城市與軟件的復雜度有可比之處,還在于其結構的復雜性。不同風格與不同類型的建筑,雜亂如蜘蛛網一般的城市道路,還有居民生存的復雜生態圈,展現出形態各異的風貌,甚至每一條陋巷都背負了滄桑厚重的歷史。
軟件系統的代碼行即磚瓦,通信端口即車輛行駛的道路,每個構建模塊是建筑物,基礎設施是排水系統,公共模塊是醫院、學校或者公園,軟件架構就是對整個城市的規劃和布局。
因而要理解軟件系統的復雜度,也可以結合理解力與預測能力這兩個因素來幫助我們思考。在軟件系統中,是什么阻礙了開發人員對它的理解?想象一下,團隊招入一位新人,這位新人就像一位游客來到了一座陌生的城市,他是否會迷失在阡陌交錯的城市交通體系中不辨方向?倘若這座城市不過只有房屋數間,一條街道連通城市的兩頭,實則是鄉野郊外的一座村落,那還會使他生出迷失之感嗎?
所以,影響理解力的第一要素是規模。
1.1
? ?
規模
軟件的需求決定了系統的規模。當需求呈現線性增長的趨勢時,為了實現這些功能,軟件規模也會以近似的速度增長。
由于需求不可能做到完全獨立,這種相互影響相互依賴的關系使得修改一處就會牽一發而動全身。
就好似城市的一條道路因為施工需要臨時關閉,此路不通,通行的車輛只得改道繞行,這又導致了其他原本已經飽和的道路因為涌入更多車輛,超出道路的負載從而變得更加擁堵,這種擁堵現象又會順勢向這些道路的其他分叉道路蔓延,形成一種輻射效應的擁堵現象。
以下幾種情況都可能使軟件開發產生擁堵現象,或許比道路堵塞更嚴重。
函數存在副作用,調用時可能對函數的結果作了隱含的假設。
類的職責繁多,不敢輕易修改,因為不知道這種變化會影響到哪些模塊。
熱點代碼被頻繁變更,職責被包裹了一層又一層,沒有清晰的邊界。
在系統的某個角落里,隱藏著伺機而動的 Bug,當誘發條件具備時,就會讓整條調用鏈癱瘓。
在不同場景下,會產生不同的異常場景,每種異常場景的處理方式都各不相同。
同步處理與異步處理代碼糾纏在一起,不可預知程序執行的順序。
這是一個復雜的生態環境,新的需求變化就好似在南美洲亞馬孫河流域熱帶雨林中的蝴蝶,輕輕扇動一下翅膀,就在美國得克薩斯州掀起了一場龍卷風。面對軟件復雜度的“蝴蝶效應”,我們心存畏懼。
在我負責設計與開發的 BI(Business Intelligence)產品中,我們需要展現報表(Report) 下的所有視圖(View)。這些視圖的數據可能來自多個不同的數據集(Data Set),而視圖的類型也多種多樣,例如柱狀圖、折線圖、散點圖等。
在這個“逼仄”的報表問題域中,我們需要滿足如下業務需求。
在編輯狀態下,支持對每個視圖進行拖曳以改變視圖的位置。
在編輯狀態下,允許通過拖曳邊框調制視圖的尺寸。
當單擊視圖的圖形區域時,應當使當前圖形的組成部分顯示高亮。
當單擊視圖的圖形區域時,應當獲取當前值,對屬于相同數據集的視圖進行聯動。
如果打開鉆取開關,則應當在單擊視圖的圖形區域時獲取當前值,并根據事先設定的鉆取路徑對視圖進行鉆取。
能夠創建篩選器這樣的特殊視圖,通過篩選器選擇數據,對當前報表中所有相同數據集的視圖進行篩選。
這些業務需求都是我們事先預見到的,無一例外,它們都是對視圖進行操作,這就導致了多種操作之間的糾纏與沖突。例如,高亮與級聯都需要響應相同的 Click 事件,鉆取同樣如此,與之不同的是它還要判斷鉆取開關是否已經打開。而在操作效果上,如果高亮與鉆取僅針對當前視圖本身,則聯動與篩選就會因為當前視圖的操作影響到同一張報表下其他屬于相同數據集的視圖。對于拖曳操作,雖然它監聽的是 MouseDown 事件,但該事件卻與 Click 事件沖突。顯然,實現這些功能的復雜度不能僅以功能點的增加來衡量。
軟件復雜度會受到需求與規模的正向影響,但它的增長趨勢要比需求與規模更加陡峭。
倘若需求還產生了事先未曾預料到的變化,我們又沒有足夠的風險應對措施,那么在時間 緊迫的情況下,難免會對設計做出妥協,頭疼醫頭,腳疼醫腳,在系統的各個地方打上補丁,從而欠下技術債。當技術債務越欠越多,累計到某個臨界點時,量變就會引起質變, 整個軟件系統的復雜度達到巔峰,步入衰亡的老年期。許多遺留系統(Legacy System)就 掙扎在瀕臨死亡的懸崖邊上。這些遺留系統符合飼養場的奶牛原則:
奶牛逐漸衰老,最終無奶可擠;然而與此同時,飼養成本卻在上升。
這意味著遺留系統會逐漸隨著時間的推移,不斷地增加維護成本。
一方面,隨著需求 的變化,對遺留系統的維護變得越來越捉襟見肘;
另一方面,系統的知識又逐漸被腐蝕。團隊成員變動了,留存在他們大腦中的系統知識隨之而去。文檔呢?勤奮而尊重流程的團隊或許編寫了可謂圣經一般完整而翔實的文檔,可惜我們卻只能參考,而不可盡信,因為這些文檔不過是刻在船舷上的印跡,雖然刻下了當時寶劍落下的位置,然而舟船已經隨著槳聲欸乃滑向了彼岸。似乎只有代碼才是最忠實的,然而當遭遇佶屈聱牙、晦澀難懂的代碼時,當需要解開如一團亂麻般的依賴關系時,我們又該何去何從?
需求的變化,知識的流逝,正是遺留系統之殤!
我曾經參與過某大型金融機構客戶系統的技術棧遷移。為了保證我們的技術棧遷移沒有破壞系統的原有功能,需要為系統的核心功能編寫自動化測試以形成保護網。
當時,曾經參與過該系統開發的人員已經“遺失”殆盡,我們除了得到少數團隊人員的有限支持, 還可以參考和借鑒的只有這個系統的數百頁 Word 文檔以及千萬行級的 Java 代碼庫。Java 代碼庫經歷了大約七八年的變遷,并主要由外包團隊開發,涉及的平臺與框架包括 EJB 2、 Spring 3.0、Struts,乃至 JDK 5 之前的 Java 代碼;
除此之外,還有部分我們完全搞不懂的 COBOL 代碼(COBOL 語言? 是在遠古時代吧!)。閱讀代碼庫時,我們常常震驚于龐大臃腫的類,許多類的代碼行數超過一萬行以上,而數千行的方法體也是屢見不鮮,并沿襲了原始時代的編程傳統,常常在方法的首端定義了數十個變量,并在整個方法中被重復賦值、 修改。系統通過 IBM MQ 實現分布式系統之間的集成。子系統之間傳遞的消息被定義為各式沒有任何業務意義的消息編碼,諸如 S01、S02、P01、P02。我們需要查閱文檔了解這些 消息代碼代表的業務含義,還需要明確消息之間傳遞的流程以及處理邏輯。
我們在為合并客戶賬戶場景編寫自動化測試時,發現文檔中描述的異常消息 S05 的處理邏輯與實際的運行結果不一致。無奈之下,我們只有通過閱讀源代碼尋找業務的真相。
這個過程仿佛福爾摩斯探案,我們不能放過代碼中任何可能揭示真相的蛛絲馬跡。運行已經編寫好的自動化測試,結合跨進程的調試手法,通過打印控制臺日志來復現消息的走向, 從而通盤了解業務流程的運行軌跡。最后,真相水落石出,而我們發現為了編寫這個自動化測試,足足耗費了兩個人日的時間。
軟件規模的一個顯著特征是代碼行數。然而,代碼行數常常具有欺騙性。如果需求與代碼行數之間呈現出不成比例的關系,則說明該系統的生命體征可能出現了異常,例如代碼行數的龐大其實可能是一種肥胖癥,它可能包含了大量的重復代碼,這或許傳遞了一個需要改進的信號。
我在做一個咨詢項目時,曾經利用 Sonar 工具對該項目中的一個模塊進行了代碼靜態 分析,如圖 4.2 所示。
圖 4.2
這個模塊的代碼行數達到了四十多萬行,其中重復代碼竟然達到了驚人的 33.9%,超 過一半的代碼文件混入了重復代碼。顯然,這里估算的代碼行數并沒有真實地體現軟件規模,相反,因為重復代碼的緣故,可能還額外增加了軟件的復雜度。
Neal Ford 在文章 Emergent design through metrics 中談到了如何通過指標來指導設計。文中提及的 iPlasma 是一個用于面向對象設計的質量評估平臺,或許我們可以通過該工具的指標(見表 4.1)來找到評價軟件規模的要素。
表 4.1 iPlasma 的指標及說明
在面向對象設計的軟件項目里,除了代碼行數,包、類、方法的數量,繼承的層次以及方法的調用數,還有常常提及的圈復雜度,都或多或少會影響到整個軟件系統的規模。
1.2
? ?
結構
你去過迷宮嗎?相似而回旋繁復的結構使得本來封閉狹小的空間被魔法般地擴展為一個無限的空間,變得無窮大,仿佛這空間被安置了一個循環,倘若沒有找到正確的退出條件,循環就會無休無止,永遠無法退出。
許多規模較小卻格外復雜的軟件系統,就好似這樣一座迷宮。此時,結構成了決定系統復雜度的關鍵因素。
結構之所以變得復雜,多數情況下還是系統的質量屬性決定的。例如,我們需要滿足高性能、高并發的需求,就需要考慮在系統中引入緩存、并行處理、CDN、異步消息以及支持分區的可伸縮結構。倘若我們需要支持對海量數據的高效分析,就得考慮這些海量的數據該如何分布存儲,并如何有效地利用各個節點的內存與 CPU 資源執行運算。
從系統結構的視角看,單體架構一定比微服務架構更簡單,更便于掌控,正如單細胞生物比人體的生理結構要簡單數百倍一樣。
那么,為何還有這么多軟件組織開始清算自己的軟件資產,花費大量人力物力對現有的單體架構進行重構,走向微服務化呢?
究其主因, 不還是系統的質量屬性在作祟嗎? 縱觀軟件設計的歷史,不是分久必合,合久必分,而是不斷拆分、繼續拆分、持續拆分的微型化過程。
分解的軟件元素不可能單兵作戰。怎么協同,怎么通信,就成了系統分解后面臨的主要問題。如果沒有控制好,這些問題固有的復雜度甚至會在某些場景下超過因為分解給我們帶來的收益。如圖 4.3 所示,由于對系統進行了分解,各個子系統或模塊之間形成了復雜的通信網結構。
圖 4.3
要理清這種通信網結構的脈絡,就得弄清楚子系統之間的消息傳遞方式,明確消息格式的定義;同時,這種分布式的部署結構,在實現這些功能的同時,還必須額外考慮跨進程通信可能出現的異常場景,例如如何確保消息的可靠傳遞,如何保證數據結果的一致性。換言之,系統因為結構的繁復而增加了復雜度。
微服務的最終一致性
基于 CAP 理論,微服務這種分布式架構在滿足 A(Availability)與 P(Partition Toralence) 的前提下,至少要保證數據的最終一致性,即系統中的所有數據副本經過一定時間后,最終能夠達到一致的狀態。
分布式架構的通信特點讓我們必須要認為網絡通信是不可靠的,這就導致在實現一致性上,微服務比傳統的單體架構要復雜得多。假如采用補償模式來實現數據的最終一致性, 就需要引入一個額外的協調服務,它負責協調各個需要保證一致性的微服務,其職責為協調服務并按順序調用各個微服務,如果某個微服務調用異常(包括業務異常和技術異常), 就取消之前所有已經調用成功的微服務。同時,還需要考慮取消操作也可能失敗的情況, 即補償過程本身也需要滿足最終一致性,這就要求在服務調用出現異常后,取消服務至少要被調用一次,而取消服務操作本身則必須是冪等的。
為了實現補償模式,我們需要記錄每次業務操作,同時還要確定失敗的步驟與狀態, 以便于定位補償的范圍。為了提高正常業務操作的成功率,還需要在設計時考慮引入重試 機制。服務執行失敗的原因各有不同,重試機制也需要提供與之對應的策略。例如對于系 統繁忙的異常,我們應采用等待重試機制;對于一些出現概率非常小的罕見異常,可以考慮立刻重試;如果失敗原因是由于某種業務原因導致的,那么即使重試也不可能保證操作成功,應采取終止重試策略。顯然,這些機制都會因為微服務的分解而帶來設計上的額外成本,它必然會導致整個系統的結構變得更加復雜。有得必有失,軟件世界的自然規律其實是公平的。
在考慮微服務設計時,業界普遍認為服務分解與組織結構要保持一致,即遵循康威定律:
任何組織在設計一套系統(廣義概念上的系統)時,所交付的設計方案在結構上都與該組織的溝通結構保持一致。
Sam Newman 認為是“適應溝通路徑”使康威原則在軟件結構與組織結構中生效 1 的。他分析了一種典型的分處異地的分布式團隊,整個團隊共享單個服務的代碼所有權。由于分布式團隊的地域和時區界限,使得溝通成本變高,團隊之間只能進行粗粒度的溝通。當協調變化的成本增加后,人們就會想方設法降低協調/溝通成本。直截了當的做法就是分解代碼,分配代碼所有權,分處異地的團隊各自負責一部分代碼庫,從而更容易地修改代碼。團隊之間會有更多關于如何集成兩部分代碼的粗粒度的溝通,最終,與組織結構內的溝通路徑匹配所形成的粗粒度 API 形成了代碼庫中兩部分之間的邊界。
注意,匹配設計方案的團隊是負責開發的團隊,而非使用軟件產品的客戶團隊。在軟件開發中,常常會遇見分布式的客戶團隊,例如不同的部門會在不同的地理位置,他們的使用場景也不盡相同,甚至用戶的角色也不相同,但在對軟件系統進行架構設計時,卻不能想當然地按照用戶角色、地理位置或部門組織來分解模塊(服務),并以為這遵循了康威定律。設計人員錯誤地把客戶的組織結構視為系統模塊(服務)的分解依據。
我曾經參與過一款通信產品的改進與維護工作。這款產品為通信運營商提供對寬帶網的授權、認證與計費工作。該產品的終端用戶主要有兩種角色:營業廳的營業員與購買寬帶網服務的消費者。
該產品的最初設計就自然而然地按照這兩種不同的角色劃分為后臺管理系統與服務門戶兩個完全獨立的子系統,而在這兩個子系統中都存在資費套餐管理、客戶信息維護等業務。
這種不合理的軟件系統結構劃分,屬于典型的職責分配不合理,不僅會產生大量重復代碼,還會因為結構失當而帶來許多不必要的通信與集成,增加軟件系統的復雜度。
國際報稅系統的架構演進
在我參與的一個國際報稅系統中,就根據用戶的角色進行了系統分解。針對報稅人, 設計了 Front End 模塊提供報稅等終端業務,而 Office End 模塊則面向業務人員和系統管理者,如圖 4.4 所示。
圖 4.4
隨著需求增多,功能越來越復雜,系統各個模塊的邊界開始變得越來越模糊,形成了一個邏輯散亂的龐大代碼庫。重復代碼與重復數據俯拾皆是,而 Front End 與 Office End 之間的集成也非常復雜。負責開發這兩個模塊的團隊雖然屬于同一個項目組,但團隊之間存在極大的技術和業務壁壘,團隊成員對整個系統缺乏整體認識,知識沒有能夠在團隊之間傳遞起來。
當通過引入 Bounded Context 來劃分模塊的邊界,建立公開統一的 REST 服務后,遵循康威定律為分解開的服務建立特性團隊(Feature Team)就演變為順其自然的結果。整個系統中各個服務的重用性和可擴展性得到了更好的保障,服務與 UI 之間的集成也變得更加簡單。整個架構清晰可見,如圖 4.5 所示。
圖 4.5
無論是優雅的設計,還是拙劣的設計,都可能因為某種設計權衡而導致系統結構變得復雜。唯一的區別在于前者是主動地控制結構的復雜度,而后者帶來的復雜度是偶發的,是錯誤的滋生,是一種技術債,它可能會隨著系統規模的增大而導致一種無序設計。
在 Pete Goodliffe 講述的“兩個系統的故事:現代軟件神話” 中詳細地羅列了無序設計系統的幾種警告信號:
代碼中沒有顯而易見的進入系統中的路徑。
不存在一致性,不存在風格,也沒有統一的概念能夠將不同的部分組織在一起。
系統中的控制流讓人覺得不舒服,無法預測。
系統中有太多的“壞味道”,整個代碼庫散發著腐爛的氣味,是在大熱天里散發著刺激氣體的一個垃圾堆。
數據很少被放在使用它的地方。
經常引入額外的巴羅克式緩存層,目的是試圖讓數據停留在更方便的地方。看一個設計無序的軟件系統,就好像隔著一層半透明的玻璃觀察事物一般,系統中的軟件元素都變得模糊不清,充斥著各種技術債。細節層面,代碼污濁不堪,違背了“高內 聚、松耦合”的設計原則,導致許多代碼要么放錯了位置,要么出現重復的代碼塊;架構層面缺乏清晰的邊界,各種通信與調用依賴糾纏在一起,同一問題域的解決方案各式各樣, 讓人眼花繚亂,仿佛進入了沒有規則的無序社會。
架構與代碼評審
我曾經為一個制造業客戶開發的業務工具項目提供架構與代碼評審的咨詢服務。當時,該工具產品的代碼庫只有不到三萬六千行代碼,是一個簡單的基于 ASP.NET 開發的 BS (Brower/Server)架構系統。雖然項目規模并不大,但是在經歷了約半年的開發周期后,項 目質量與交付周期都不能得到足夠的保證。在之前交付的版本中,位于歐洲的銷售代表普遍對這個工具不滿意,所以客戶希望我們能夠在技術層面上提供一些咨詢建議。
該工具產品的開發存在諸多問題,例如在領域層充斥著大量的貧血對象,對框架的強依賴導致“供應商鎖定”,在技術選型上也多有不當之處。但最大的問題還是系統缺乏清晰的邊界,如圖 4.6 所示。
圖 4.6
架構師雖然采用了經典的三層分層架構模式對關注點進行分離,卻沒有很明確地勾勒出各個分層的明確職責,開發人員也沒有按照這種分層架構來分配職責,在本來應該是視 圖呈現的代碼中混入了許多領域邏輯,從而導致 UI 層越來越臃腫。而在領域層,卻又不恰當地滲入了對 ASP.NET UI 組件的處理邏輯。該產品代碼庫存在的另一個問題是缺乏一致性。例如針對數據庫的訪問,產品竟然提供了如下三種不同的解決方案。
1. Utils 訪問方式,其代碼如圖 4.7 所示。
圖 4.7
2. DbHelper 訪問方式,其代碼如圖 4.8 所示。
圖 4.8
3. ORM 訪問方式,其代碼如圖 4.9 所示。
圖 4.9
顯然,選擇這三種迥然不同的訪問方式并非出于技術原因,又或者受到某個質量屬性的約束,而是在設計時沒有做到統一的規劃,開發人員率性而為,內心會自然而然地選擇自己最熟悉、實現成本最低的技術方案,從而導致訪問數據庫的解決方案不一致。
1.3
? ?
變化
我們之所以不能預測未來,是因為未來總會出現不可預測的變化。這種不可預測性帶來的復雜度使得我們產生畏懼,因為不知道何時會發生變化,變化的方向又會走向哪里, 所以導致心里滋生一種仿若失重一般的感覺。變化讓事物失去控制,受到事物牽扯的我們便會感到惶恐不安。
在設計軟件系統時,變化讓我們患得患失,不知道如何把握系統設計的度。若拒絕對變化做出理智的預測,那么系統的設計會變得僵化,一旦變化發生,修改的成本就會非常大;若過于看重變化產生的影響,渴望涵蓋一切變化的可能,則一旦預期的變化不曾發生, 我們之前為變化付出的成本就再也補償不回來了。
從需求的角度講,變化可能來自業務需求,也可能來自質量屬性,而以對系統架構的 影響而言,尤以后者為甚,因為它可能牽涉整個基礎架構的變更。George Fairbanks 在《恰如其分的軟件架構》一書中介紹了郵件托管服務公司 RackSpace 的日志架構變遷,雖然業務功能沒有任何變化,但是郵件數量卻持續增長,為了滿足性能需求,架構經歷了三個完全不同的系統變遷:從最初的本地日志文件,到中央數據庫,再到基于 HDFS 的分布式存儲,整個系統幾乎發生了顛覆性的變化。這并非 RackSpace 的架構設計師欠缺設計能力, 而是在公司草創之初,他們沒有能夠高瞻遠矚地預見到客戶數量的增長,導致日志數據增多,以至于超出了已有系統支持的能力范圍。
俗話說“事后諸葛亮”,當對一個軟件系統的架構設計進行復盤時,總會發現許多設計決策是如此愚昧。殊不知這并非愚昧,而是在設計之初,我們手中掌握的籌碼不足以讓自己贏下這場面對未來的戰爭罷了。這就是變化之殤!(未完待續)
本文節選自中生代技術社區出版架構圖書:架構寶典,根據篇幅略有刪節
往期推薦
歐創新:深度解析DDD中臺和微服務設計
領域驅動專家張逸文字脫口秀:簡單工廠不簡單
DDD專家張逸:《解構領域驅動設計》前言
Hacker News熱文:請停止學習框架,學習領域驅動設計(DDD)(獲500個點贊)
京東平臺研發朱志國:領域驅動設計(DDD)理論啟示
DDD專家張逸:構建領域驅動設計知識體系
領域驅動設計(DDD)在美團點評業務系統的實踐
當DDD遇上微服務
DDD戰略篇:架構設計的響應力
可視化與領域驅動設計
領域驅動設計(DDD)前夜:面向對象思想
領域驅動設計(DDD):領域和子域
? ?END ? ?? #技術人必備#點個在看,讓更多人看見
總結
以上是生活随笔為你收集整理的DDD专家张逸:复杂与架构演进的关系的全部內容,希望文章能夠幫你解決所遇到的問題。