色哟哟视频在线观看-色哟哟视频在线-色哟哟欧美15最新在线-色哟哟免费在线观看-国产l精品国产亚洲区在线观看-国产l精品国产亚洲区久久

0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

循序漸進搭建復雜B端系統整潔架構

京東云 ? 來源:jf_75140285 ? 作者:jf_75140285 ? 2024-11-18 17:11 ? 次閱讀

前言:信息時代技術更迭和傳播速度不斷加快,技術變得泛娛樂化,大數據、云計算、區塊鏈、元宇宙、大模型,一代代技術熱點在社會輿論的裹挾之下不斷地吸引著資本的眼球,技術人員為了不被時代所淘汰也不得不時刻追趕潮流。在這樣一個時代背景下,軟件工程作為一門不起眼到有些枯燥的古老學科,似乎早已被開發者們遺忘在角落。作為一名技術人員我們自然應該時刻保持對前沿技術的追蹤,然而,當發生線上問題我們卻面對著成片的屎山代碼毫無頭緒時;當業務方提出個性化需求我們卻因為不敢對系統做出修改而強迫對方做出妥協時;當一次請求處理流程中出現多達數萬次重復地數據庫操作而影響到整個系統的穩定性時,大家都應該沉下心來思考一下,我們是不是忘記了作為一名程序員的初心和對代碼的極致追求。 還記的當年我抱著朝圣的心態從傳統行業踏入京東職場時的興奮與期待,然而這份期待很快就被四處可見的屎山代碼給澆滅了,后來從朋友口中了解到其他頭部互聯網廠商的業務系統其實也是半斤八兩。這似乎是軟件行業中的一個電車難題,一邊是無盡的業務需求和倒排的工期,一邊是補丁摞補丁的糟糕代碼,是繼續泡在醬缸中縫縫補補還是向屎山代碼說不,開發人員被困在中間不知該如何抉擇。然而事實上,追求整潔架構與提升研發效率之間從來就不是一個悖論。正如Robert C.Martin在其著作《Clean Architecture》中所說:“不管你多敬業、加多少班,(在面對爛系統時)你仍然會寸步難行,因為你大部分的精力是在應對混亂(而不是在開發需求)?!痹斐晌覀冋占影嘹s需求和疲于應對線上問題的根本原因,恰是那些不被我們重視的糟糕代碼。業務天然就是復雜的,這決定了軟件系統的本質復雜度(Essential Complexity),這種復雜度是無法通過軟件架構去消除的。那么解決上述問題的關鍵就是找到某種架構去引導開發者對復雜業務進行問題拆解,分而治之,在這個基礎上再通過標準規約和工具約束及輔助開發者寫出可理解、易拓展、好維護的代碼,以此來對抗軟件系統本身的偶然復雜度(Accidental Complexity,Frederick P.Brooks,Jr, 《The Mythical Man-Month》)。 為了找到這樣的一種架構,我們從19年就開始對各類架構思想和實踐案例進行了深入地學習和探索,并在接下來的3年時間里通過局部架構演進的方式進行了大量的實踐驗證,在這個過程中我們對這些架構思想的理解也從早期的懵懂教條式執行逐漸做到了如今的融匯貫通,并最終在22年底形成了一套成體系的框架及方法論,并在京東廣告投放平臺重構工作中進行了實戰應用。本文也將以廣告投放平臺架構升級作為背景案例,從設計思想到落地框架,循序漸進地為您介紹這套新架構的誕生始末,而這套架構思想的演進歷程則在《改進我們的架構》一文中有詳細的闡述。

一、架構升級背景

與高并發請求給C端系統帶來的系統高性能、高可用能力挑戰相比,B端系統所面臨的挑戰則是如何在海量多維度、多模塊、多場景融合的復雜業務需求中保持系統健康、穩定、快速地迭代。京東廣告投放平臺就是一個典型的復雜B端業務系統,它承擔著集成廣告業務體系中各個垂直業務模塊,構建、維護和分發廣告物料的重要職責。經過多年迭代,京東廣告投放系統目前已集成40余個垂直業務系統,支撐7條核心產品線,先后賦能10余個獨立投放平臺,維護著一個擁有200余個業務實體的龐大數據模型,每天都需要處理海量長事務、多系統交互的復雜業務請求。同時作為整個廣告業務鏈路上的首發環節和功能門面,廣告投放系統每年都要承接400余個來自不同業務方的差異化需求、執行1000余次代碼合并及600余次功能發布。在極高的需求密度之下,作為撬動廣告主預算的重要戰場,投放系統在為廣告主提供優秀投放體驗的同時,還需要每天向廣告業務鏈路穩定輸送PB級的物料數據,這對系統的性能、穩定性以及團隊的研發效能提出了極高的要求。

wKgaomc7BH6AVYDVAANn0HkT3GI202.jpg

廣告投放平臺是一個典型的多平臺、多模塊集成的復雜B端系統

二、傳統架構的研發痛點

近年來隨著技術和業務的飛速發展,新的廣告業務形態和投放組件層出不窮,廣告物料結構愈發復雜。與此同時,為了提高廣告主留存和撬動預算,業界各大平臺都在向著極簡版、智能化和集成化的方向發展。這些新的業態發展方向一方面給廣告主帶來了更加便捷和流暢的投放體驗,另一方面也讓投放系統內部業務流程愈發復雜,如何用有限的研發人力快速支撐越來越多的多場景復雜業務需求成為各大廣告投放平臺必須要解決的關鍵問題。然而傳統的“三層架構+面向數據庫編程”的研發模式由于過于簡單的封裝及粗暴的設計思想在面對這些高復雜度業務需求時變得愈發吃力,逐漸成為阻塞研發效能提升的罪魁禍首。

客觀:傳統架構面對高復雜度的業務時毫無應對之法

作為一個典型的Web應用,廣告投放系統長期以來采用的都是傳統的三層架構,這種沒有架構的架構極其簡單、易上手,因此一直以來都是業界的主流。但是由于它缺少統一明確的邏輯拆分與封裝工具,業務的復雜度會等比滲透到代碼實現中,進而導致系統的代碼復雜度飆升,模塊之間隨意耦合,邏輯糾結纏繞,經過幾輪迭代之后就成了看不懂、動不了、不敢動的醬缸代碼。這些看似基礎的編碼問題實際上卻是阻礙我們研發效能提升的罪魁禍首:

1.需求交付提速困難:不同平臺、產品線及業務場景邏輯交織,晦澀難懂,導致系統功能迭代時梳理及設計耗時漫長,同時在測試階段需投入大量精力進行聯動功能回歸;

2.對新業態的接受度低、響應能力差:系統拓展性差,且模塊間深度耦合,在面對新業態時我們卻為了控制影響范圍而不得讓業務選擇讓步,結果錯失商機。

3.問題評估和排查效率低下:缺少明確統一的邏輯歸屬與封裝準則,邏輯四處復寫與逃逸,導致問題定位時間長,難以快速評估影響范圍和修復方案。

4.接口性能與穩定性下降:混亂的封裝與復用導致一次接口請求就會產生導致大量重復的IO操作,嚴重影響接口性能,每臨大促都需要花費大量人力進行性能優化。

主觀:“面向數據庫編程”的設計思維讓系統加速腐化

我們的業務本質就是獲取、處理、存儲及傳輸數據,在傳統架構中業務邏輯通常以事務腳本(Transaction Script)的形式實現:業務規則直接在開發者的大腦中轉化為數據庫的增刪改查操作(這也是很多程序員調侃自己是CRUD工程師的原因),然后被寫到代碼里。這種模式在場景單一、需求簡單的業務發展早期階段可以快速實現功能,但是隨著業務復雜度的提升,這種過于粗糙的設計思維所帶來的問題就會逐漸顯現出來:

1.難以建立對整個數據模型的全景認知:完整的數據模型信息被拆分到不同的業務接口實現中,往往需要對整個工程代碼進行逐行review才能梳理出完整的數據模型,當工程代碼量和數據模型膨脹到一定程度后,模型梳理成本急劇飆升。

2.模型野蠻膨脹、存在大量相似或重復的實體,增加系統運維成本:數據模型全景認知的缺失導致開發者難以進行統一的頂層設計,數據模型泛化表達能力弱,在多需求并行開發過程中極易形成信息孤島,無法實現模型合并與共享,系統中存在大量相似的業務實體與庫表結構。

3.代碼對業務語義表達能力弱、業務知識傳承效率低下:代碼經過開發者的轉譯失去了對業務語義的直接表達,導致系統中存在大量只有開發者本人才能理解的魔法邏輯,系統維護與人員更迭成本過高。

要想解決上述問題,就亟需一種面向未來的架構思想來指導我們對系統進行全面地升級。在此背景下,業界眾多平臺紛紛進行了領域驅動設計思想的探索和嘗試,經典的案例有阿里的星環與COLA、快手的Baldr等,京東也推出了藏經閣平臺與Matrix框架。這些實踐案例和架構迭代路線給了我們很多啟發,本著腳踏實地、事實就是的基本原則,在經過充分調研和長期驗證之后,我們立足于京東廣告業務的本質特征推出了一套可復用的復雜B端業務支撐框架,其核心內容可以分為PICASO能力編排框架與聚合及資源庫機制兩部分。

網絡上能夠找到很多介紹領域驅動設計思想的文章,但是大多都聚焦在對領域驅動設計中眾多術語和概念的介紹上,對領域驅動設計思想的落地實踐卻淺嘗輒止。再加上中英文語境的差異和國內外軟件開發生態的不同,都在很大程度上將領域驅動設計思想“妖魔化”了,讓很多同學望而卻步或者不得其要義。然而我們在摸索實踐的過程中逐漸意識到,領域驅動設計作為一種軟件架構設計的指導思想其實并沒有創造什么新的東西,而是對基本的軟件設計思想進行的系統化總結和升華。但正是這種系統性的歸納將各類技巧、準則和思想凝練成體系化的方法論,并且在行業內形成了被所有開發者所公認的行為準則,這才是領域驅動設計思想強大生產力的源泉和魅力所在。 與傳統的三層架構相比領域驅動設計思想其實并沒有復雜多少,其要義就在于保持業務、模型與代碼三者的統一,只要掌握了這一點,領域驅動設計思想中的各種理念都將是水到渠成的事情。初讀《領域驅動設計》時書中眾多晦澀的術語也曾讓我十分困惑,但其中的很多內容其實已經是很多優秀架構師的工作日常了。隨著對領域驅動設計思想理解的逐漸深入,我不時會產生“咳,這說的不就是xxx么”的感慨。這也是沒有辦法事,誰讓國外那些提前入局的大佬們牢牢掌握著專業領域的命名權呢。也正因為如此,在本文中我們不會去介紹、甚至會盡量避免引用領域驅動設計理論中的術語,避免大家一開始就陷入到那些晦澀難懂的概念里而無法自拔。希望大家能將更多的精力放在框架內各個模塊的設計動機與運行機制上,這才是我們最應該思考和關注的內容。至于領域驅動設計思想在新架構演進過程中的指導作用我們將會在《領域驅動設計與PICASO框架》一文中進行詳細地介紹。

三、升級措施

(一)PICASO框架:從混亂到有序,構建圖書館式的代碼架構

圖靈獎得主Frederick在其著作《The Mythical Man-Month》中將軟件系統的復雜度劃分為本質復雜度(Essential Complexity)和偶然復雜度(Accidental Complexity),其中本質復雜度是問題本身所具有的復雜度,與求解方法無關,而偶然復雜度是求解方法引入的復雜度。本質復雜度無法避免,但是我們可以通過優化求解方法來盡可能降低系統的偶然復雜度。這給了我們很大的啟發,業務天然就是復雜的,這是一個客觀事實,架構設計的目標不是消除業務上的本質復雜度,而是應該引導和輔助開發者更好的拆解和分析業務帶來的復雜度(是handle而不是eliminate)。同時,軟件架構應該提供足夠靈活的標準規約與框架工具,讓所有開發者都能夠按照統一的思想寫出可理解、易拓展和好維護的代碼,減少甚至是消除由于沒有封裝或封裝不統一帶來的偶然復雜度。在這一思想的指導之下,經過兩年多的打磨,我們推出了PICASO框架。

PICASO概述

PICASO是一套以領域驅動設計(Domain-Driven Design, DDD)作為思想內核,專門為集成式復雜業務系統設計的通用基礎框架。它的命名來自“PICASOIs aContextualAbilitySeparate andOrchestrate Framework(PICASO是一種基于上下文的能力分解與編排框架)”的首字母縮寫。有趣的是這個縮略詞的發音恰好與西班牙現代派繪畫大師畢加索(Picasso)的姓名讀音相同,畢加索在畫作中經常對人體部位進行解構和重組,在接下來的介紹中我們將發現這一點與PICASO框架所強調的能力拆分與編排思想有異曲同工之妙,而這也是我們最終采納這個命名的原因。

PICASO的命名啟發自筆者比較喜愛的一個開源項目——WINE,其功能是通過內核適配器在Linux環境中運行Windows應用程序,其命名也是這種藏頭詩的風格:WINEIsNotEumlator(WINE不是模擬器)。

PICASO框架的職責是引導開發者將復雜業務流程正交分解為多個簡單子問題,然后將這些簡單子問題的處理邏輯封裝為邊界明確的標準可執行實體,在PICASO框架中這些可執行實體被稱為領域能力。完成能力拆解之后,開發者可以通過PICASO提供的能力編排框架將不同的領域能力的組合成一個完整的請求處理流程,這個處理流程所在的可執行實體就是一個領域服務。領域服務會為每次請求生成一個上下文對象,通過這個上下文對象可以在不同領域能力以及領域能力與領域服務之間進行數據傳遞與共享,進而避免重復及碎片化的IO操作。PICASO框架還提供了開箱即用的通用可執行實體發現與路由組件,開發者可以通過該組件按功能域對領域能力及領域服務進行分組和聚合,每個分組對外暴露統一的請求路由門面,從而向上層調用實體屏蔽分組內部的場景復雜度,進而實現復雜度降維。如果在領域能力或領域服務的路由維度之外還存在其他維度的細微邏輯差異,開發者可以通過PICASO提供的拓展點機制進一步實現差異點分離。同樣的,拓展點依然可以接入通用可執行實體發現及路由組件,向上層實體屏蔽拓展點所在功能域內的場景復雜度。

上文對PICASO框架的整體架構進行了整體地介紹,接下來我們將從軟件系統復雜度根源分析開始,循序漸進地詳細闡述PICASO各個模塊的設計動機及運行機制。

wKgZomc7BH-Afl6xAAR-OzAak40998.png

PICASO框架整體架構

復雜度的根源

軟件設計的本質就是持續對抗軟件本身產生的復雜度,早在最開始進行新架構探索的時候我們就意識到,構建整潔架構的前提是厘清系統復雜度的根源。

本質復雜度

通過對復雜業務系統發展歷程的分析,我們發現業務復雜度一般來自水平方向上的多維度拓展和垂直方向上的多模塊集成。

業務發展的早期往往都是單一場景,隨著業務的發展,產品形態開始變得豐富多樣,服務的用戶及業務方也越來越多,業務架構從原來的單點結構逐漸演變為復雜的樹狀結構,樹的每一層都代表一個業務維度,業務的發展讓系統在水平方向上呈現出多維度增長的特征。以廣告投放系統為例,最初的投放系統只有合約展示包段一種業務形態,隨著程序化廣告和智能廣告的興起,廣告投放及播放形式層出不窮,業務樹中開始出現“產品線”的維度;而為了服務不同業務方,我們在系統中增加了“投放平臺”的維度;對不同投放標的物的支持又在系統中引入了“計劃類型”的維度......就這樣廣告投放系統的業務架構也逐漸演變成了如下圖所示的復雜樹狀結構。

wKgaomc7BIKAbpi7AAVvle-4rwE522.png

多維度、多模塊、多場景的廣告投放業務

而在垂直方向上,早期的業務流程一般比較簡短,只有少數幾個業務環節。隨著業務的發展,系統功能越來越豐富,業務流程也變得愈發冗長,開始呈現出鮮明的模塊化特征。同樣以廣告投放系統為例,早期的廣告物料只有時段、預算、出價、創意幾個基礎模塊,隨著業務的發展陸續新增了智能出價、人群定向、地域定向、商品定向、智能創意、智能選品等業務模塊,物料創編流程也越來越冗長。除此之外,單個模塊內部也開始出現多場景分化,如廣告投放系統中的智能出價模塊內部就存在tCPA、tROI、eCPC、MC等不同的智能出價模型,其數據模型及業務規則也不盡相同,這進一步增加了業務的復雜度。

本小節從業務架構演進歷程的視角分析了業務復雜度的來源,這構成了系統的本質復雜度。而對這些復雜業務規則的實現方案(好的、或者是壞的)就成了系統偶然復雜度的來源。

偶然復雜度

業務在多個維度上向著熵增的方向不斷發展,但是我們的代碼始終只有一套,不同維度的業務場景可能對同一個業務環節提出不同的個性化需求,造成不同維度的業務邏輯互相耦合,代碼中開始出現大量層層嵌套的if-else分支,圈復雜度不斷飆升,系統開始出現腐化跡象。此時一些工程師可能會意識到這個問題并開始著手優化,但是由于缺少統一的邏輯封裝與拓展工具,再加上開發者的水平與技法也不盡相同,導致優化方案五花八門,這種方案上的不一致反而進一步增加了代碼的復雜度。除此之外,隨著系統集成的業務模塊越來越多,業務流程愈發冗長,與外部子系統的交互邏輯越來越復雜,開發者不得不去處理超時、重試、冪等、長事務、分布式事務及跨系統的數據一致性等問題,這些技術方案的引入對系統來說也是復雜度的來源。

對架構設計的啟發

從上面的論述中可以看出,系統偶然復雜度的高低在很大程度上取決于開發者能否分析處理好業務的本質復雜度,另外在多人協作開發場景中,軟件架構的標準性和解決方案的一致性也是決定系統偶然復雜度的重要因素,這就是我們推出PICASO框架的根本原因。我們希望PICASO能夠引導開發者對復雜業務流程進行模塊化拆解,采用分治思想逐一擊破,并通過標準的邏輯封裝規約與框架來實現多維度邏輯拓展,讓團隊中每一位開發者都能夠以統一的思想寫出清晰、簡潔、有序、可檢索的代碼。

到這里相信有些讀者可能會產生一些疑問,既然軟件系統的偶然復雜度是技術方案本身的復雜度,那么引入PICASO框架是否也在增加系統的偶然復雜度呢?答案是肯定的,新框架的引入的確會增加系統的偶然復雜度。PICASO框架由于采用了全新的設計思想,在推行早期曾經歷過痛苦的磨合期,也出現過不少由于開發者不理解新架構的運行機制而導致的設計缺陷或線上問題。但是任何架構迭代之路都是螺旋上升的,新技術帶來的系統復雜度畢竟是靜態的,隨著開發人員對新架構運行機制及使用技巧的逐漸掌握,系統便開始趨于穩定,新技術帶來的優化收益也會逐漸顯現出來。但是如果我們不對現有的架構做出升級,那么系統將隨著源源不斷的業務需求向著不可控熵增的方向不斷發展,由此帶來的系統復雜度將是動態且持續增加的。

PICASO的復雜度應對之道

在分析完系統的復雜度來源之后,接下來我們將詳細介紹PICASO如何協助開發者對抗軟件系統的復雜度。IEEE對軟件架構的定義為:架構是由系統之間的組織、組件及組件之間的關系、以及對設計與演進的指導原則組成的,其中前兩者是具體的實體框架,后者是指導思想。而軟件架構的指導思想往往決定著前兩者的實現,對指導思想的理解與掌握程度也直接決定了開發者能否在實際業務中用好架構。以Spring框架為例,Spring的指導思想為:控制反轉(IoC)、依賴注入(DI)及面向切面編程(AOP),這三大核心思想一方面直接決定了Spring框架核心模塊的實現,另一方面也是開發者要想用好Spring則必須掌握的內容。而對PICASO來說,其指導思想可以概括為:能力拆分、拓展點抽象及能力編排。

wKgZomc7BIKAKmCGAAFlQ3Aps5Y369.png

軟件架構的構成

領域能力拆分與路由助力多模塊集成

神經認知學家喬治·米勒在他的論文《神奇的數字7》中指出人腦能夠同時處理的信息容量是有限的,人腦的短時記憶容量為7(7個數字、6個字母或5個單詞),后來的研究更是將這個數字降到了4個左右。所以當冗長的業務流程疊加上多維度的個性化訴求,系統的業務復雜度將飆升為

,這顯然超出了我們大腦的瞬時處理容量,此時就需要利用

關注點分離

分類

分層

思想對復雜問題進行求解。

分離

關注點分離(Separation of concerns,SOC)就是把復雜問題正交分解為多個互不相關的最小子問題,聚焦整體問題的局部復雜性,逐步進行求解。我們在《復雜度的根源》章節中指出,復雜的業務系統往往會呈現出鮮明的模塊化特征,因此我們可以自然而然地根據業務模塊的功能邊界對冗長的業務流程進行拆分,然后聚焦單個模塊進行設計與抽象,避免陷入多模塊、多場景互相耦合的思維泥沼。PICASO框架為此引入了領域能力及領域服務的概念,其中領域能力用來承接單個業務模塊內部的邏輯細節,而領域服務則負責通過組合不同的領域能力實現一個完整的業務流程。如下圖所示,以單元新建流程為例,我們可以把單元新建流程劃分為:單元基礎信息構造、優化目標設置、出價設置、人群設置、地域定向設置和商品定向設置多個子模塊,我們可以將這些模塊內部邏輯封裝成領域能力,然后通過這些能力的組合構建一個完整的單元信息領域服務。

chaijie_default.png

一個完整的業務流程可以拆分為多個原子業務模塊,每個原子業務模塊還可以按照其內部的業務模式進行進一步細分

PICASO框架中的領域服務與DDD思想中的領域服務是同一個概念,其職責和定位都是承接無法在單個實體與值對象內部直接實現的業務邏輯(事實上,B端系統對外提供的大部分服務都無法在單個聚合內直接實現)。而領域能力的概念則經常出現在一些企業級中臺化框架中,如阿里的星環、京東的Matrix等。盡管當年如火如荼的中臺化戰略如今已經偃旗息鼓,但是我們還是將這個命名引入到了PICASO中,因為我們確實沒有找到一個比它更合適的命名,可以如此形象地描述一個足夠內聚、自治且能夠被復用和拓展的原子實體。當然PICASO中的領域能力與那些企業級中臺化框架中的領域能力相比要輕量和易用的多,不需要繁瑣的身份申請,也不存在跨工程熱加載的問題,畢竟中臺化的重心在管理域平臺及前中臺團隊的協作上,而PICASO則始終聚焦在代碼本身的復雜度控制上。其實中臺化也好,組件化也罷,系統的復雜度就擺在那里,不管用什么由頭,要想提升團隊整體的研發效能,它都是我們必須要去解決的一個問題。在本小節的論述中,領域能力似乎就是根據業務模塊的邊界簡單劃分出來的。但是在實際開發中的能力劃分要復雜的多,需要綜合考慮能力的應用場景、會被哪些領域服務使用、以及能力之間的依賴關系等諸多因素進行反復地推導和調整。本文對能力劃分方法論只是簡單地做了問題引入,更加具體的內容我們將在《PICASO框架最佳實踐——能力識別與劃分》一文中進行詳細介紹。

領域能力的拆解除了能夠降低業務流程分析的復雜度之外,也提高了代碼復用和拓展的靈活性。領域能力就像積木一樣,可以被組裝到不同的領域服務中,如人群設置能力可以同時被單元新建服務、單元編輯服務、人群快捷修改等領域服務復用。而能力拆解帶來的拓展靈活性性是相對于樸素模版設計模式而言的。在傳統架構中模板類可能是我們使用最多的設計模式,它的確能夠簡單有效地實現復用共性流程、分離差異的目標。但是由于復雜業務流程中不同業務節點的差異化維度往往是不同的,直接將業務主流程抽象成一個模板類,將各個節點作為模板中的抽象方法,那么該模板類子類的繼承關系復雜度將是各個業務節點內部場景復雜度的叉乘。再加上傳統架構并沒有積極引導開發者落實面向對象編程的思想,導致我們基本上還在以面向過程的方式開發我們的系統,通常會將同一個產品線中不同的業務方法實現到同一個Service或者Manager類中,這將進一步加重模板抽象及子類繼承關系的復雜度。而PICASO框架通過領域能力拆解將不同的業務環節拆分到了單獨的原子業務實體中,將模板中的抽象方法算子化。由于不同的原子業務模塊之間互相正交、互不干擾,因此能夠讓這些業務算子獨立迭代,在各自的業務維度上靈活地進行繼承和拓展。

分類

只是把業務流程按照功能邊界拆分成不同的模塊通常是不夠的,因為單個模塊內部往往還存在細分的業務模式,如上圖中的出價設置模塊,其內部還存在手動、MC、tCPA、eCPC等不同的出價模型,這個時候就需要根據分類思想進行進一步拆解。分類思想是關注點分離思想進一步的延伸,它在分離的同時還注重元素之間的共性特征。當模塊內部出現場景分化時,PICASO框架建議開發者對模塊進行進一步細分,將模塊內不同場景的業務規則封裝為不同的能力實例。這些能力實例之間盡管存在邏輯差異,但是畢竟屬于同一個原子業務模塊,在數據模型、接口協議乃至業務流程上都存在很大的相似度。因此PICASO會將同模塊下不同業務場景對應的領域能力實例聚合到一起,這樣的一組能力被稱為一個能力節點。同一個能力節點下的各個能力實例使用相同的接口參數及上下文定義,每個能力節點下會額外定義一個能力門面,能力門面通常不承載具體的業務規則,它僅負責定義當前能力節點對外的接口協議以及從請求參數中提取業務場景標識的邏輯,它是能力節點下所有能力實例對外提供服務的統一入口。上層的領域服務組合領域能力時,引用的不是具體的領域能力實例,而是各個能力節點下的能力門面。PICASO框架內置的可執行實體發現與路由機制會在應用啟動時掃描出系統中所有的能力門面,并建立好能力門面與各個能力實例的路由表。當請求到來時,領域服務不必關注本次請求應該使用哪個具體的能力實例,而是直接調用能力門面的統一入口,PICASO框架會通過內置的可執行實體發現與路由機制提取請求中的場景標識,然后將請求路由到對應的領域能力實例上,從而實現模塊內部的場景復雜度與領域服務模塊集成復雜度之間的解耦。以出價模塊為例,出價模塊內部會根據不同的出價類型細分為tCPA、MC、eCPC等智能出價能力實例,但是單元新建領域服務并不會直接操作這些具體的能力實例,它引用是出價設置能力門面。當請求到來時,PICASO框架會根據請求中的出價類型自動將請求路由到相應的能力實例上??蓤绦袑嶓w發現與路由機制是PICASO框架內置的一個底層通用組件,是能力編排、拓展點機制等頂層功能的基礎。其本質上就是一個增強型的門面+策略模式,我們通過一些實現技巧將其做成了一個可以適配任意可執行實體的通用組件。如下圖所示,單元新建業務流程涉及標的物設置、出價設置及人群設置等業務環節,這些業務環節內部都有各自的細分場景。在代碼實現中,這些業務環節被抽象為3個能力節點,節點內部的細分場景被隔離到不同的能力實例中,在構建領域服務時就不需要考慮當前各個能力節點下的細分邏輯,只需要專注于業務流程本身,實現各個能力門面的組裝邏輯即可。

wKgaomc7BIWAOOvzAAEihCV9iHs039.png

能力門面與能力實例的抽象實現了能力編排復雜度的降維

分層

分層則是分類思想在領域服務、拓展點等其他實體粒度上的延伸。如快車、互動、推薦三條產品線的單元新建服務會被劃分到同一個服務分組下,對外暴露一個單元新建服務門面。這樣做目的是下層實體對上層實體暴露統一的門面接口,自下而上地逐層屏蔽下層實體的內部復雜度,實現維度間復雜度解耦,進而將代碼的整體復雜度由

降維到

。下圖展示了PICASO框架中各個業務實體的層級結構,最下層是各個領域能力實例執行器,它們承載了具體的業務規則;相同功能子域的領域能力實例會對上層的領域服務實例暴露一個統一的領域能力門面執行器,領域服務實例執行器會通過組合領域能力門面定義具體的業務流程;而相同的功能子域的領域服務實例又會對領域服務統一入口(Domain Service Faced)暴露一個領域服務門面執行器,屏蔽模塊內領域服務實例之間的細分規則;領域服務統一入口將不同的領域服務門面集成到一起,對上層不同的流量來源暴露統一的請求入口。

這種分層結構讓開發者逐層解構業務復雜度的同時,實際上也構造了一個索引結構,為實現可檢索的代碼架構打下基礎。

wKgZomc7BIaAfGvkAALPFsd84Dc491.png

自下而上逐層屏蔽層級內部的業務場景復雜度

在分層架構中,除了可以通過通用可執行實體路由機制自下而上地屏蔽下層實體的內部場景復雜度之外,有時我們還要反過來自上而下地進行復雜度合并。我們用一個例子來說明這種設計技巧:在廣告投放業務中有一個經典的出價計算器模塊,它會根據廣告物料上的基礎出價、人群溢價、關鍵詞出價、流量包溢價、時段溢價等信息預估廣告物料最終的出價值范圍。計算邏輯只有一個,但是由于計算邏輯關聯了眾多底層模塊,物料新建、修改以及關聯模塊的快捷修改、還有對物料進行修改過程中實時出價預估回顯(此時最新的修改并未落庫)等接口都會調用出價計算器模塊,但是這些使用場景對出價預估參數的填充程度是不同的,需要模塊針對不同的使用場景執行不同的參數補充查詢邏輯。這是一個典型的上層模塊的調用場景復雜度滲透到底層模塊實現復雜度中的例子。在分層架構中,越是底層的模塊在設計上需要考慮的場景應該越少,而且要避免與上層模塊的使用場景耦合。因為上層模塊的使用場景是動態增加的,不知道什么時候就會有新的使用場景出現,而底層模塊的真正需要處理的內部場景應該比頂層使用該模塊的場景要少且穩定的多。所以解決這個問題的措施就是底層模塊面向自己內部的業務模式在參數中定義一個隱式的標識屬性,讓調用方根據自己的使用場景和業務訴求隱式地設置該參數,底層模塊則直接根據參數中的這個標識屬性執行相應的分支邏輯?;氐匠鰞r計算器的案例中,該模塊的使用場景有:單元新建后事件觸發、單元整體修改后事件觸發、單元新增關鍵詞后事件觸發、單元新建中臨時觸發、單元修改中臨時觸發、單元添加關鍵詞中臨時觸發等多種使用場景,未來也不確定會出現什么新的使用場景。但是對出價計算器模塊的內部計算邏輯來說,其實只有需要補充查詢單元下關鍵詞信息和不補充查詢這兩種場景。為此我們在出價計算器能力參數中增加一個布爾類型的參數,能力內部直接根據該參數判斷是否需要執行關鍵詞的查詢操作,出價計算器模塊的調用方則分別根據自己的使用場景判斷該如何設置這個參數,從而起到自上而下的合并上層調用場景復雜度、保持底層模塊穩定的作用。

有些讀者或許會覺得這種機制與上文介紹的能力路由機制是互相矛盾的,然而他們實際上并不沖突。因為對那些能夠自下而上屏蔽內部場景復雜度的模塊而言,它們通常顯式地定義了內部不同業務模式的標識屬性,如出價模塊的出價類型、人群定向模塊的人群類型等,用戶在請求參數中也會顯式地設置請本次請求對應的業務標識,因此框架能夠直接對這些模塊應用通用可執行實體路由機制。但是實際業務中也存在一些模塊,它們內部沒有定義明確的業務模式標識,而是根據請求來源、調用場景等動態條件執行不同的業務邏輯。此時我們可以先暫時忘掉這些模塊的調用場景,而是聚焦模塊內部的業務分支提煉出隱藏其中的業務模式,然后讓上層模塊將動態調用場景轉化為底層模塊定義的隱式業務模式標識參數,接下來就能繼續應用通用可執行實體路由機制了。因此,分層思想中自下而上屏蔽的是模塊內部的固有場景復雜度,而自上而下合并的則是模塊外部的使用場景復雜度,二者其實是互補的關系。

拓展點機制協助走出多維度泥潭

領域服務與領域能力的路由機制能夠較好的應對系統多模塊集成帶來的復雜度,但是領域能力及領域服務必須嚴格遵守框架規約,繼承標準業務執行器模版(后續章節會有詳細講解),定義出明確的數據交換協議及上下文對象,這些都是相對較重的操作。因此能力或服務路由的維度必須抓住最核心的業務差異,而不是把所有存在業務差異的維度都納入到路由規則中,否則就會造成沙?;鸱?,反而增加系統的維護成本。因此我們還需要一種機制能夠以更加輕量的方式承載除了領域服務或能力路由維度之外其他業務維度上的細微差異,這就是拓展點機制要解決的問題。

拓展點機制是通用可執行實體發現與路由機制在更細粒度上的延伸應用,本質上就是將存在差異化邏輯的環節抽象為一個接口從主流程中分離出去,然后將不同場景的差異化邏輯隔離在不同的拓展點接口實現中,這其實就是依賴倒轉原則(Dependence Inversion Principle, DIP)的應用。與領域服務和領域能力相比,拓展點的定義和實現成本都要低很多,框架對拓展點接口內的方法及方法參數都不會做過多的約束,定義一個拓展點僅需要繼承框架提供的標準接口并指定路由標識的提取邏輯,而實現一個拓展點接口時也僅需要在實現拓展邏輯之外額外指定當前拓展點實例能適配哪些路由標識。拓展點可以嵌入到領域服務、領域能力以及資源庫(Repository,下文中會詳細闡述)中任何一處存在差異化邏輯的流程中。

拓展點機制作為能力與服務拆分路由機制的補充,支持任意維度上的差異化邏輯隔離。以下圖為例,在廣告投放系統中,底層的人群設置能力節點已經按照其核心屬性人群類型進行了能力實例的劃分。由于系統還賦能了多個投放平臺,不同的投放平臺對可綁定的人群上限有著不同的限制,此時就可以將各個能力實例中人群綁定數量校驗環節抽象為一個拓展點接口,以投放平臺類型作為路由KEY為各個投放平臺提供不同的接口實現,從而自上而下地解決多維度拓展的難題。

這里說的“自上而下”是一種形象的描述,可以理解為父層級業務維度內不同的業務場景在子層級模塊上產生的差異化邏輯。但實際上拓展點機制并不限制邏輯的維度拓展方向,如下圖的例子中,右側觸點新建服務領域服務實例所屬的服務門面定義的服務路由維度是產品線,但是不同的計劃類型的單元新建流程之間依然存在細微的邏輯差異,此時盡管計劃類型是產品線的子維度,但是依然可以通過拓展點來承載這些細微的邏輯差異。

wKgaomc7BIeASER0AAXRp-5dtmU414.png

拓展點機制的核心作用是作為能力及服務路由維度的補充,進一步實現差異點的分離

能力編排框架確保架構思想切實落地

前面幾個小節一直在論述如何對復雜邏輯進行拆解和分離,但是系統要想對外提供可用的功能,就必須再次把這些分離出來的能力及拓展點組合起來,構成一個完整的領域服務。最簡單的組合方式就是直接硬編碼依次調用各個能力門面的功能入口,手動實現前置方法調用結果與后置方法入參的屬性映射和轉換,但是這種組合方式會在業務主流程中插入大量的膠水代碼,稀釋代碼的信息密度,將流程關鍵節點掩蓋在大量繁瑣無趣的`setter`、`gettter`方法調用中。為了解決這個問題,同時確保新架構設計思想能夠精準落地,讓規范和標準框架化,PICASO自建了能力編排框架,它為前文所述的各類思想落地提供了框架基礎,將前文提到的各種實體、組件與設計思想有機結合到一起,自動實現模塊串聯,讓開發者專注于業務邏輯本身,實現填空式開發,最終構建出一個完整的工程應用。

目前業界有很多流程編排引擎,有老牌廠商的Netflix Conductor、AWS Step Function等,也有開源的Apache Activiti、Zeebe等。我們在早期架構探索階段對這些解決方案也進行了調研和試用,但是發現它們都無法滿足我們的訴求:以輕量級的方式實現模塊組合,提高模塊與組件的復用性,同時凸出呈現核心業務流程,輔助開發者快速抓住業務主線并建立對業務的全景認知。這很大程度上是由于上述開源組件的定位大都是接口級別的服務編排或者是審批流之類的流程引擎,因此其實現方案或執行成本往往較重,很多流程編排框架過分強調通過UI框架拖拽式實構建業務流程,導致開發者需要先在代碼工程中實現業務組件,再到UI界面中構建串聯流程,適用的場景有限且造成強烈的割裂感不說,開發者依然需要手動配置組件之間的參數映射與數據傳遞邏輯,而脫離了開發工具的代碼提示與補全功能,這些邏輯的實現成本反而增大了。與這些問題相比,拖拽式的UI界面雖然炫酷,但并不是我們的核心訴求。還有一些編排框架采用了中心化的部署方式,流程串聯與組件服務分離部部署,通過RPC實現組件調用,這種方式會付出巨大的網絡開銷及中間結果存儲成本。這種設計讓它們在批處理任務場景中有較好的應用,但是在交互式服務應用場景中則會造成嚴重的性能問題并付出巨大的運行成本。因此,在經過一次次嘗試之后我們最終決定舉起自研大旗,開發一套與PICASO架構基本思想相適配的能力編排框架。

wKgaoWc7BImAMJ3pAA1anm7UCsw016.png

要想滿足我們在上文中提出的能力編排相關的訴求,能力編排框架需要提供兩個基本功能:分別是在編碼階段通過簡潔、直觀、易用的API輔助開發者定義業務流程,以及在請求處理階段根據開發者制定的執行圖串聯各個業務組件完成請求處理流程。為了實現這兩個基本功能,PICASO框架采取了制定標準化業務執行模版、內嵌標準上下文機制以及自建能力編排框架三項舉措。

標準業務執行器模版

標準業務執行器模版立足于軟件系統的內在本質定義了適用任何業務場景的基本處理流程,就像Object對象在JDK中的作用一樣,標準業務執行器模版并不復雜,但它卻是PICASO框架中所有組件功能得以實現的基礎。從本質上看,所有的軟件系統都在做三件事:數據的獲取、處理與存儲(或傳輸);從業務視角看,數據的處理又可細分為輸入數據的合法性校驗以及數據的計算與轉換,而數據的合法性校驗又可細分為對輸入數據直接進行的校驗以及需要結合系統內外部詳情數據進行的校驗?;谏鲜稣撌?,PICASO框架定義的業務處理的基本流程為:

1.參數預校驗:直接對請求入參進行的校驗,這些校驗邏輯通常都是簡單的內存計算,不依賴任何外部數據,如參數完整性校驗、參數值范圍校驗、數據長度校驗等。

2.上下文初始化:基于校驗后的入參查詢數據詳情并填充到上下文中,如根據入參中的單元ID查詢單元詳情、根據userId獲取賬戶詳情等,這些數據將會在后續流程中使用。

3.基于上下文的業務校驗:執行需要結合上下文中詳情數據才能進行的業務校驗,如根據單元狀態判斷是否可以執行物料的修改操作、判斷標的物類型與物料計劃類型是否匹配等。

4.業務邏輯處理:基于參數及上下文中的詳情數據執行領域模型(下一章節介紹)的構造和修改,注意對于一些查詢類的服務,這個步驟可能不是必須的。

5.數據持久化:將新建或修改后的領域模型保存到數據庫中或者調用外部服務API完成數據傳遞,這同樣是一個可選的標準步驟,另外有些服務在業務邏輯處理環節就已經完成了數據傳遞。

6.發布領域事件:一些修改類的領域服務在完成請求處理之后可能需要通知其他領域內的業務實體做一些相關的后置操作,PICASO框架是通過領域事件機制來實現這個功能的(后續章節中進行詳細介紹)。

7.構造處理結果:業務流程執行完成后構建返回給調用方的響應數據。

標準業務執行器模版本質上就是一個Executor模板類,上述基本業務流程也就是該模板類中主要的模板方法。在PICASO框架中,領域服務和領域能力都要繼承標準業務執行器模板類,這樣做的目的是引導和約束開發者對領域服務和領域能力的具體實現邏輯按照標準業務執行流程進行二次拆分,從而可以讓框架對代碼進行精細化地調用控制。標準業務執行模版是對所有業務處理流程進行的最頂層抽象,模版類中各個標準業務執行步驟API的制定讓把不同業務模塊的串聯執行職責從開發者手中轉義到框架手中成為可能,開發者不必手動實現不同模塊和方法的串聯調用,而是專注于業務邏輯,實現填空式開發,從而減少系統中的膠水代碼,提高信息密度,這本質上就是依賴倒轉原則(Dependency Inversion Principle, DI)的應用。下面的代碼片段給出了標準業務執行器模版的定義,出于突出呈現PICASO框架設計思想的目的,示例代碼去除了框架功能的具體實現邏輯,僅保留了核心要素及模版方法的定義。

/** * 標準業務執行器模板基類,定義了基本的業務處理流程,所有領域服務和領域能力執行器都必須繼承該類。 * * @param 業務執行器對應的參數類型,所有的執行器參數都應該繼承自標準參數基類Command對象 * @param 業務執行器最終返回的執行結果類型 * @param 業務執行器使用的上下文對象類型,所有執行器的上下文對象都應該繼承標準上下文基類, * 請求的入參和產生的中間結果都會保存在上下文對象中 */ public abstract class CommandExecutor > { /** * 參數預校驗,該步驟應該只進行純內存計算操作 * @param context 上下文,此時的上下文中只有參數對象 */ protected Response doPreValidate(CTX context) { return Response.success(); } /** * 執行上下文初始化,根據參數執底層情數據的拓展查詢,并將查詢結果填充到context對象中 * @param context 上下文,調用該方法時的上下文中只有參數對象,調用完成后上下文將被填充 */ protected Response doInitContext(CTX context) { return Response.success(); } /** * 結合上下文中的底層數據執行業務校驗 * @param context 上下文,此時的上下文中已經完成了依賴的業務詳情數據的填充 */ protected Response doContextualValidate(CTX context) { return Response.success(); } /** * 結合上下文中的底層數據執行業務邏輯的處理,對已有實體的變更及生成的新業務實體都會填充回上下文對象中 * @param context 上下文,業務邏輯執行過程中的中間結果也可以暫存到到該上下文中 */ protected Response doProcessBizLogic(CTX context) { return Response.success(); } /** * 保存業務流程執行過程中新建或者被修改過的業務實體,調用該方法時,這些數據已經被寫入到了上下文對象中 * @param context 上下文 */ public Response doPersistAggregates(CTX context) { return Response.success(); } /** * 構造本次業務請求流程中需要對外發布的領域事件 * @param context 上下文 */ protected Response> doPublishAppEvent(CTX context) { return Response.success(); } /** * 構造請求的返回值 * @param context 上下文 */ protected Response doAssembleResponse(CTX context) { return Response.success(); } }

到這里有些讀者可能還沒有意識到“把不同業務模塊的串聯調用職責從開發者手中轉移到框架手中”的價值,這項措施其實并沒有直接解決我們在本文第二章提出任何一個痛點問題,要想理解這一措施我們必須用辯證法重新審視前文介紹的各項復雜度應對措施。根據前面幾個小節的論述,我們為了應對業務復雜度而對請求處理流程進行了各種粒度和場景的拆分,拆分出來的各類實體再疊加上實體內部標準執行步驟的二次拆解,必然會增加后續邏輯串聯和組裝的復雜度。如果此時還要求開發者手動硬編碼實現邏輯組裝,那么勢必會帶來極高的開發負擔和出錯概率,而且硬編碼組裝帶來的大量膠水代碼還會稀釋和掩蓋代碼中的關鍵信息,后續再進行迭代時就容易產生改動點遺漏和影響評估不全等問題。PICASO框架解決這些問題的措施就是由框架代替開發者實現合個模塊的串聯組裝,這也我們要定義標準業務執行器模版的根本原因:統一標準的調用入口是讓框架實現流程串聯的前提,進而才可能實現將膠水代碼隱藏在框架內部、提高業務層代碼信息密度、降低開發者編碼負擔的設計目標。PICASO框架內模塊串聯的詳細邏輯我們將在本章后三個小節中進行闡述,在那之前我們先繼續介紹標準業務模版中的其他核心要素。

上下文機制

從標準業務執行模版的示例代碼中我們可以看到,除了各個標準步驟的方法聲明之外,標準業務執行模版還通過泛型變量定義了執行器接收的請求參數類型、返回值類型以及上下文對象類型,其中“上下文”是標準業務執行器模版中的核心要素,業務數據就是通過它在各個標準步驟之間流轉的。所謂的上下文本質上就是一個POJO(Plain Old Java Object),其內部定義了業務流程執行所需要的各種詳情數據。在《改進我們的架構》一文中我們已經對上下文機制進行了詳細的闡述,在這里我們簡單回顧一下它的作用。如下圖所示,在傳統架構中開發者往往直接面向數據庫編程,業務邏輯與數據庫操作互相交織,容易造成重復或碎片化的數據庫讀寫操作。而采用上下文機制之后,業務流程中的各個子模塊都不再封裝數據的讀寫操作,而是在請求一開始先將后續流程所需要的數據集中初始化到上下文對象中,后續各個業務模塊統一從上下文中獲取所需的詳情數據,并把產生的中間結果寫入到上下文對象中,最后在所有子模塊業務邏輯執行完成之后,集中將上下文中新增或發生變化的業務實體持久化到存儲介質中。這種設計一方面能夠避免子模塊劃分導致的重復及碎片化的數據讀寫操作,另一方面,集中的數據操作可以啟發開發者采用批量、異步和并行等措施進行極致地性能優化。

wKgZoWc7BIyATkcrAAQsIVuLcfI607.png

上下文機制與傳統架構業務處理流程對比

PICASO框架對上下文機制做了進一步升級和深度集成,上下文作為核心要素被直接定義到標準業務執行器模版中,領域服務和領域能力執行器都要通過泛型參數來聲明自己所需的上下文對象類型。需要說明的是,盡管領域能力執行器中也定義了“上下文初始化”標準步驟,但是PICASO框架依然建議開發者盡量在領域服務執行器的上下文初始化步驟中就將各個領域能力所依賴的業務實體或外部數據集中批量查詢好,然后填充到領域服務上下文中。后續各個領域能力會優先從領域服務上下文中獲取所需的詳情數據,領域能力的上下文初始化步驟僅做依賴數據的非空校驗或者做為從領域服務上下文中獲取不到所需數據時的托底補充查詢措施,這是由于領域服務作為整個業務流程的全局把控者,擁有最全的數據視角,可以對代價昂貴的IO操作進行極致地調優,而領域能力則聚焦于業務流程的局部細節,在能力內部封裝的數據讀寫操作很容易隨著能力的組合或循環復用而被碎片化或重復地執行。

需要注意的是上文中“領域能力優先使用領域服務上下文中的數據”并不意味著領域能力會直接訪問外層領域服務的上下文對象,這是由于同一個領域能力可能會被不同的領域服務所復用,因此領域能力不可能與其中任何一個領域服務的上下文耦合到一起。為了解決這個問題,PICASO框架要求每個領域能力都要定義自己專有的上下文對象。在調用領域能力之前先將領域服務上下文中的數據傳遞到領域能力的上下文中,領域能力中的業務邏輯直接訪問的依然是領域能力自己的上下文對象,在能力執行過程中構建的新實體或者對已有實體的修改也會直接保存到領域能力上下文中。而在完成能力調用之后,PICASO會將領域能力上下文中新生成或者發生變更的屬性傳遞回領域服務上下文中,從而在保持領域能力與領域服務解耦的前提下實現領域服務與領域能力上下文數據的共享。因此在PICASO框架中上下文機制除了起到避免碎片化及重復讀寫數據的作用之外,還負責在不同領域能力以及領域服務與領域能力之間進行數據的傳遞和共享。

我們可以用一個例子來詳細描述上述機制,如下圖所示,領域服務內編排了三個領域能力:A、B、C,其中能力A和C分別依賴業務實體1和實體4,能力B依賴能力A生成的數據實體2,完成業務邏輯處理后框架需要把能力B和C構建的業務實體3和5以及能力C對實體4的修改保存到數據庫中。當請求到來時PICASO框架會首先調用領域服務的上下文初始化標準步驟(initContext)完成實體1與實體4的查詢,在調用能力A之前會將實體1從領域服務上下文拷貝到能力A上下文中,完成能力A的調用后會將其構建的實體2從能力A的上下文中拷貝回領域服務上下文,然后將領域服務的上下文作為兩個能力之間數據共享和交換的通道,在調用能力B之前將實體2拷貝到能力B的上下文中......以此類推用相同的方式完成能力B和能力C的調用,最后PICASO框架會調用領域服務的聚合根持久化標準步驟(persistAggregate),集中將新生成的實體3和5以及由能力C修改后的實體4持久化到數據庫中。

wKgaoWc7BIyAWh1qAAGOJeSo7fQ622.png

領域服務與領域能力上下文之間的數據傳遞關系

上下文機制作為PICASO框架的基礎組件,其內部除了用戶自定義的業務屬性之外還承載著大量框架內部運行所需的狀態數據以及大量為開發者提供的工具API。在本文中我們僅對其基本運行機制進行了介紹,關于上下文的使用技巧及其基類中各種工具API的使用方法,我們將在《PICASO框架最佳實踐——上下文機制》一文中進行詳細的闡述。

標準業務模版執行引擎

在上一小節的最后我們通過一個架空的例子論述了PICASO框架內部的數據傳遞流程,這些數據傳遞規則并不需要開發者手動實現,而是通過PICASO框架內置的標準業務模版執行引擎自動觸發的。接下來我們就將詳細闡述標準業務模版執行引擎是如何將領域服務及領域能力各個標準步驟串聯到一起的。但是在此之前,我們有必要必再次明確領域服務和領域能力執行器的職責,這對理解標準業務模版執行引擎的設計動機十分重要。

在PICASO框架中,系統對外提供的服務都是由領域服務執行器承載的,作為整個業務流程的全局把控者,領域服務執行器的基本職責就是定義業務流程(編排組裝領域能力)以及管理業務數據(上下文的初始化及持久化),而領域能力執行器則聚焦在完整業務流程中的某個特定模塊,負責實現該模塊內部具體的業務規則。如前文所述,領域服務是通過領域能力組合編排而成的,并且它們都繼承了標準業務執行器模版,因此不難推導出領域服步驟其實就是通過各個領域能力的相應標準步驟組合而成的。但是這并不意味著業務流程中所有的業務邏輯都會下沉到領域能力中,比如領域服務上下文初始化操作就必須在領域服務執行器中直接定義。此外,考慮到領域能力聚焦于局部業務細節,無法獨立對外提供服務,為了明確組件職責,避免給開發者帶來困惑,領域能力執行器對標準業務執行器模版進行了二次拓展,隱藏了完整請求流程處理維度才需要的聚合根持久化(persisteAggregates)、構建并發布領域事件(publishAppEvent)以及組裝請求響應數據(assembleResponse)標準步驟,因此這三個模版方法對應的業務邏輯也需要直接在領域服務執行器中定義。

領域服務及領域能力執行器的職責劃分決定了二者之間的數據傳遞時機及其標準步驟之間的組合關系,標準業務模版執行引擎的模塊串聯規則就是基于此制定的。其核心設計就是對業務流程中各個領域能力標準步驟的重組執行。在PICASO框架的早期摸索階段,我們曾傾向于將同一個業務模塊的參數校驗與業務處理邏輯劃分到兩個不同的領域能力中。這種能力劃分方式固然也能實現業務功能,甚至也能起到復雜度分離的作用,但是這種劃分方式會造成業務邏輯的沙粒化分解,產生大量瑣碎的小能力,這反而會增加系統的開發及維護成本。另外,由于同一個模塊的參數校驗及業務處理邏輯往往會依賴相同的底層數據,沙礫化的能力劃分會急劇增加能力間數據傳遞和共享的負擔,稍有不慎就會造成數據的碎片化讀寫,進而對系統性能產生影響。因此我們最終選擇回歸業務本質,以最小原子業務邊界作為能力劃分的準則,將同一模塊內關聯緊密的參數校驗、上下文初始化、上下文校驗及業務處理邏輯封裝到同一個領域能力執行器中。但是這種封裝規則卻帶來了新的問題:領域服務由領域能力組合而成,如果我們直接依次串行調用每個領域能力內的各個標準步驟,將無法實現領域能力與領域服務標準步驟之間的協調執行,另外由于調用后置能力時前置能力所有標準步驟都已執行完畢,如果后置能力的參數校驗失敗而前置能力在業務邏輯處理步驟已經與外部系統產生了數據交互,此時就會產生臟數據等問題。然而標準業務執行模版的抽象則為我們帶來了該問題的解決方案:將領域能力執行器中的各個標準步驟拆散到領域服務執行器相應的標準步驟中重組執行,而不是依次觸發每個領域能力的全部標準步驟,如下圖所示,在領域服務的參數預校驗標準步驟中會按照能力執行圖依次觸發各個領域能力的參數預校驗步驟,而在領域服務的上下文初始化步驟中則會依次觸發各個領域能力執行器中的上下文初始化步驟。這種重組執行機制確保了服務請求流程能夠整體按照參數預校驗、上下文初始化、上下文校驗、業務邏輯處理、聚合根持久化、發布領域事件、構造返回值的標準流程執行下去,實現fail-fast特性,避免由于后置操作校驗失敗而前置操作已執行導致的IO資源浪費及臟數據問題,另外這種運行機制帶來的額外收益是讓我們能夠利用現有服務快速實現請求預校驗接口,這一點我們在本文第四章的PICASO框架開發流程示例中將有專門的呈現。

上述過程也是辯證法的生動詮釋,事物之間存在普遍聯系,在對立統一中不斷發展。PICASO框架中也是在一次次提出方案、引發新問題、解決新問題的過程逐漸成型的,框架中各個組件互相支撐,互為因果,共同實現整潔架構的最終目標。我們也希望各位讀者在閱讀本文時能夠始終將本文介紹的各項組件聯系到一起來理解框架的指導思想和設計動機,這對未來我們能否在實際業務用好PICASO框架來說十分重要。

wKgZoWc7BI6AT9T9AAffngV0LKc232.png

標準業務模版執行引擎的重組執行流程

下面我們將結合著上圖所示的能力標準步驟重組執行流程圖逐步解析標準業務流程模版執行引擎的運行機制:

1. 參數預校驗

當請求到來時,PICASO框架會首先通過領域服務門面定位到具體的領域服務執行器實例,然后調用其參數預校驗標準步驟(preValidate),在該方法中會首先執行領域服務執行器直接定義的參數預校驗邏輯(當然也可以根據開發者的設計意圖調整為先觸發各個領域能力的參數預校驗邏輯),然后再觸發領域能力執行圖中各個領域能力的參數預校驗邏輯,需要注意的是由于領域能力執行器有自己專屬的參數及上下文對象,因此在調用各個能力參數預校驗方法之前,PICASO會自動將領域服務入參對象中的屬性拷貝到領域能力入參對象中同名同類型的屬性上(與Spring框架中BeanUtils.copyProperties的邏輯相同)。

2. 上下文初始化

完成參數預校驗邏輯之后,PICASO會開始執行領域服務的上下文初始化邏輯。我們鼓勵開發者將各個領域能力所依賴的底層數據集中到領域服務的上下文初始化邏輯中批量查詢好,因為領域服務作為整個業務流程的全局把控者,擁有最全面的數據視角,可以進行最徹底的性能優化。完成領域服務直接定義的上下文初始化邏輯之后,PICASO將調用能力執行圖中各個領域能力的上下文初始化步驟,但是在此之前,與領域服務與領域能力之間的參數傳遞邏輯類似,PICASO框架會先將領域服務上下文對象中的屬性拷貝到領域能力上下文對象中同名同類型的屬性上。

有些讀者可能會對本小節的論述有些疑惑,封裝領域能力的目的之一是為了邏輯復用,然而我們卻要將其依賴數據的初始化邏輯代理到領域服務中,那么當一個領域能力被不同的領域服務引用時,是否會造成重復編碼呢?這個問題的答案是肯定的,但是絕大多數場景下領域能力依賴的底層實體通常不多,一方面我們可以通過接下來將要介紹的“聚合與資源庫”機制簡化這些底層實體的查詢邏輯,與由此收獲的性能提升收益相比,重復編碼所付出的輕微代價是完全值得的。另一方面我們其實并不建議在領域能力內部實現依賴數據的初始化查詢操作,因為能夠被編排到同一個領域服務中的領域能力通常都會依賴相同的業務實體,如果要在每一個領域能力內都實現一遍實體查詢邏輯同樣會造成重復編碼。因此我們建議還是統一由領域服務完成上下文的初始化,然后通過上下文傳遞機制拷貝到領域能力上下文中,領域能力僅在上下文校驗標準步驟中做好上下文參數的非空校驗,確保領域服務傳遞過來了正確的數據。更多關于領域服務及能力上下文數據傳遞方案設計的技巧請參考《PICASO框架最佳實踐——上下文機制》。

3. 上下文校驗

完成領域服務及各個領域能力的上下文初始化邏輯之后,PICASO會繼續執行領域服務及各個領域能力的上下文校驗邏輯。該標準步驟內執行的是需要結合上下文中的底層數據才能進行的校驗邏輯,如調整預算時要求新預算與歷史預算差值必須大于5%且必須大于當前消耗,該邏輯依賴歷史預算及物料當前消耗詳情,就可以在上下文初始化步驟完成這兩部分底層數據的查詢,然后在上下文校驗步驟中直接從上下文中取出詳情數據執行相關的校驗規則。默認情況下領域服務與領域能力在上下文校驗步驟不需要執行任何的參數或上下文傳遞操作。

4. 業務邏輯處理

基于上下文的業務校驗通過之后,PICASO框架會繼續觸發領域服務及能力執行圖中各個領域能力的業務處理邏輯。需要注意的是由于領域能力的業務邏輯處理過程中可能會對上下文中已有的實體進行了修改,也可能會構建出新的業務實體對象,這些變更最終都需要被持久化到存儲介質或者外部系統中。因此在默認情況下,PICASO會在能每一個領域能力的業務邏輯處理標準步驟執行完成之后執行一次領域能力上下文到領域服務上下文的數據回傳操作,將領域能力上下文中的屬性拷貝到領域服務上下文中同名同類型的屬性上。這些數據將在領域服務的后續步驟中被持久化到存儲介質中,或者被用于構造領域事件及請求響應結果。

6. 聚合根持久化、發布領域事件、構造響應結果

由于標準業務執行器中的剩余的幾個標準步驟承載的都是請求維度的邏輯,領域能力執行器標準模版中對這幾個方法也做了屏蔽,因此在這幾個標準步驟的執行流程中就不需要再調用領域能力執行圖了。需要特別說明的是,當執行到聚合根持久化標準步驟時,定義在領域服務及領域能力中的業務規則對實體的變更以及構建出的新業務實體都已經寫入到了上下文中,開發者可以充分利用領域服務對數據操作全局把控的職責定位,積極采用批量、異步、并行等手段進行極致地性能優化。

7. 定制化執行流程

前五步內容介紹了領域服務及領域能力標準執行模版默認的串聯執行邏輯,PICASO框架也遵循約定大于配置(convention over configuration)的基本原則,如果默認的執行邏輯能夠滿足開發者的訴求,開發者不需要實現過多的流程控制,但是要更靈活地適配各類業務場景,PICASO框架也支持開發者對上述標準串聯執行邏輯進行定制化的修改:

?首先,PICASO允許開發者指定僅執行領域服務的部分標準步驟,如前臺業務方期望能夠在實際調用系統的單元創建接口之前先對其構造出來的請求參數進行校驗提前發現問題,因此希望系統為其提供一個預校驗接口(注意這里的“預校驗”不是標準執行模版中的參數預校驗步驟),該場景就可以直接復用物單元新建領域服務執行器,并且在觸發領域服務執行器時指定間僅執行該領域服務的參數預校驗、上下文初始化及上下文校驗邏輯,領域服務完成前三個標準步驟的執行之后就會立即返回前三步的執行結果,從而快速實現業務方訴求,這其實也是標準業務執行模版抽象帶來的額外收益。

?其次,PICASO框架支持開發者對領域能力執行圖內的參數及上下文傳遞的時機與具體映射邏輯、能力調用的失敗與異常處理以及各個領域能力的觸發時機等行為進行定制化的修改,如出價設置能力與預算設置能力都依賴物料當前的消耗數據,除了在領域服務的上下文初始化步驟完成查詢的常規設計之外,也可以讓出價設置能力完成消耗數據查詢,然后以領域服務上下文作為媒介,將物料消耗數據從出價設置能力傳遞到預算設置能力中。這個時候就可以指定PICASO框架在完成出價設置能力的上下文初始化步驟調用之后立即執行一次從能力到領域服務上下文的數據回傳操作,而不必等到默認的業務邏輯處理步驟完成之后。而這些定制化的串聯執行配置都可以通過接下來將要介紹的能力編排領域特定語言來實現。

能力編排執行圖

在前一小節中我們介紹了PICASO框架內部各個原子模塊與標準步驟的串聯執行流程,PICASO框架通過內置的標準業務模版執行引擎將各個模塊的串聯執行職責從開發者手中轉移到了框架內部,從而讓開發者專注于業務規則設計,實現填空式開發。這里說的“業務規則”一方面是指領域服務與領域能力各個標準步驟內具體的業務邏輯,另一方面是要明確當前業務流程需要按照什么樣的順序執行哪些領域能力、能力執行的前置條件、對默認串聯規則的定制化配置(包括參數傳遞規則、上下文傳遞規則、錯誤及異常處理邏輯等),這些信息將以領域能力執行圖的形式提供給PICASO框架,之后框架就可以按照開發者的意圖完成對各個領域能力的串聯調用,而能力編排指的就是構建領域能力執行圖的過程。

PICASO能力編排框架的核心職責有兩個,首先是在編碼階段讓開發者能夠以易用、簡潔、直白的方式快速定義出業務流程對應的領域能力執行圖,其次是在請求處理階段將能力執行圖解析為可以被標準模版執行引擎理解的執行計劃,讓其能夠根據開發者意圖完成業務邏輯的處理。我們可以通過下圖所示的框架內部實體關系圖對上述兩項職責進行詳細闡述,圖中藍色線條標記的是領域能力執行圖的構建過程,紅色線條標記的是請求到來時領域能力執行圖的執行流程。

wKgaoWc7BI-AYG6OAAH447HlPwM876.png

PICASO能力編排框架內部實體關系圖

上圖其實不算是標準的實體關系圖,它更像是實體關系圖與流程圖的結合,其中不同顏色的線表示不同執行流程。事實上我們認為這種呈現方式更加符合現實,實體之間的關系本就是復雜的,在不同的場景和流程下實體之間的關系和相互作用往往也是不同的。

從圖中我們可以看到,每一個領域服務執行器內部都集成了一個領域能力編排器,在編碼階段,開發者可以通過它提供的領域特定語言(Domain Specific Language, DSL)以直白的方式構建領域能力執行圖。能力執行圖由多個領域能力編排節點構成,每一個領域能力編排節點內部都封裝著一個領域能力門面執行器。當請求到來時,業務模版執行引擎會首先對能力編排執行圖進行解析,根據本次請求的參數及上下文信息將執行圖中各個能力編排節點解析為零到多個領域能力執行要素,所謂的領域能力執行要素就是一個[領域能力執行器、參數對象、上下文對象]三元組,它是一個有狀態的、會話級生命周期的實體,除了核心的能力執行三要素之外,其內部還維護著在請求處理過程中執行引擎產生的一些控制中間狀態,如當前已執行到哪個標準步驟、調用過程中是否發生了異常、本次調用是否已經提前終止等。執行圖中解析出來的所有領域能力執行要素將被構造成一個領域能力執行要素鏈調用器,它用來控制各個領域能力執行要素的觸發行為,包括能力執行器各個標準步驟的逐步調用、能力編排節點的延遲解析、不同執行要素之間的并行調用、能力執行要器的提前終止等。在領域能力執行要素鏈調用器的控制之下,能力執行圖中的各個能力門面執行器被依次觸發,通過標準可執行實體發現與路由機制定位到當前請求應該使用的能力實例,調用其各個標準步驟完成業務邏輯處理。由于本文旨在介紹PICASO框架中各項組件的基本原理和運行機制,并沒有對能力編排框架的實現細節做過多探討,有關能力編排框架各項特性的詳細介紹請參考《PICASO框架最佳實踐——能力編排》。

能力編排領域特定語言

在PICASO框架設計之初,我們也曾想直接引入一些開源的流程編排框架來實現領域能力之間的串聯調用。但是正如本章節最開始論述的那樣,現有的開源解決方案并沒有滿足我們的核心關切:以直白、簡潔、輕量、易用的方式實現能力組裝,解決為了應對業務本質復雜度而采取的各項實體拆分與路由機制帶來的編碼繁瑣、模塊組裝邏輯復雜等副作用,減少膠水代碼和開發者的編碼負擔,提高關鍵業務信息密度。圖形化、配置化的流程編排框架雖然能夠直觀的呈現業務處理流程,但是也造成靈活度差、普適性低、開發流程割裂、業務知識分散、模塊串聯配置繁瑣等問題,無法達成整體熵減的設計目標,而PICASO框架解決這些問題的方案則是自定義能力編排領域特定語言。

領域特定語言(Domain Specific Language, DSL)是專門針對特定應用領域的計算機語言,與C++、Java等通用計算機語言(General Purpose Language,GPL)相比,領域特定語言的功能及普適性十分有限,但是在特定領域之內它卻具有強大的表達能力。領域特定語言的核心吸引力在于它提供了一種更清晰地傳達系統各部分意圖的方法,提高代碼的可讀性(雖然我們總是有意或者無意地低估了代碼可讀性對生產力的影響),降低開發者與領域專家(產品、測試甚至是用戶)的溝通難度。它能夠讓使用者輕松實現聲明式編程(Declarative Program),對業務層開發者來說,這意味著他們可以直接告訴框架他們想做什么,而不必編寫要想達成目的而需要執行的具體操作步驟(Martin Fowler, Domain Specific Language, 2013),也正是這一點讓PICASO框架能夠將前文所述的各項業務復雜度應對措施帶來的系統偶然復雜度屏蔽在框架內部,將PICASO框架內部的各個功能模塊有機結合到一起,共同實現整體熵減的設計目標。

軟件工程領域的大師Martin Fowler將領域特定語言分為外部DSL(External DSL)和內部DSL(Internal DSL)兩大類。外部DSL往往擁有自定義語法、需要宿主應用的代碼執行文本解析,基于該類DSL編寫的業務規則通常以腳本或配置的形式存在于系統代碼之外,典型的案例是正則表達式。而內部DSL是通用編程語言的子集,它對外提供一組特定的API,利用內部DSL編寫的業務規則往往是一段合法的代碼,典型的例子就是JDK8之后提供的Java Stream API。與外部DSL相比,內部DSL不需要專門的語法解析器和開發平臺,可以直接與宿主應用代碼無縫銜接,也能直接復用普通IDE的代碼提示與自動補全功能,也正因為此,為了向業務開發者提供集中、連貫的開發體驗,我們最終選擇為PICASO能力編排框架開發一套內部領域特定語言。

為了盡可能靈活地適配所有的業務流程構建場景,我們在PICASO框架的能力編排DSL中定義了順序、條件和循環三套能力編排邏輯,分別對應順序執行、if...else判斷、循環三種流程控制方式。其中每一類能力編排節點的配置都遵循約定大于配置的原則,按照標準業務模版執行引擎的默認執行邏輯提供了全部缺省配置,同時開發者也可以通過能力編排API定制自定義的執行邏輯,如循環規則、分支判斷條件、觸發能力時的參數及上下文傳遞邏輯、失敗及異常處理邏輯、能力節點解析步驟等。由于本文旨在介紹PICASO框架的設計思想和各模塊的底層運行機制,因此我們不會對能力編排框架所有DSL API進行詳細論述,這部分內容將在《PICASO框架最佳實踐——能力編排》一文進行詳細論述。本文僅通過一個實際的能力執行圖構建案例讓大家對能力編排DSL有一個具象的感知。

下圖給出的是一個站外字節廣告計劃創建請求處理流程對應的領域能力執行圖構建邏輯,可以看出能力編排DSL僅用數十行代碼以一種近乎白話文形式描述出了完整的計劃構建過程:首先對用戶已創建計劃數量進行上限檢查(CampaignUpperLimitCheckAbility);上限校驗通過后會構造出一個空的計劃對象并為其填充用戶ID、計劃類型等基礎信息(CampaignBaseInfoAssembleAbility);然后判斷本次計劃創建請求是否在參數中設置了聯合活動ID,如果聯合活動ID不為空則需要執行聯合活動信息設置相關的業務邏輯(CampaignJointActivityAbility);完成聯合活動信息設置之后就要依次設置計劃的投放周期(CampaignScheduleConfigAbility)、計劃名稱(CampaignNameConfigAbility)、營銷目標(CampaignMarketTypeConfigAbility)、應用集(TrafficStrategyConfigAbiliy)等模塊的相關屬性,需要注意的是在上述幾個能力的編排邏輯中,由于領域能力的參數或上下文對象中的屬性名稱與領域服務的參數及上下文中相應屬性并不匹配,默認的參數及上下文數據傳遞機制將無法為這幾個屬性設值,因此開發者對營銷目標設置能力的參數傳遞(cmdTransfer)、上下文傳遞(ctxTransfer)、應用集設置能力的上下文傳遞(ctxTransfer)邏輯進行了自定義拓展,手動實現了參數及上下文中特殊屬性的數據映射邏輯;在完成這些業務模塊的屬性設置之后,如果請求參數中設置的標的物類型為“商品庫”,那么接下來就要執行與站外DPA廣告業務相關的特殊業務環節:推廣SKU的校驗(SkuValidateAbility)以及SKU跟單信息的配置(TraceOrderSkuConfigAbility)邏輯......

wKgaoWc7BJKAYZniAAeB1kuLdEQ286.png

順序及條件能力編排DSL示例

上圖計劃新建流程的能力編排邏輯中只用到了順序和條件編排這兩種最常用的編排模式,但是在一些批量操作請求處理流程中通常還會用到循環編排模式,它允許標準業務模版執行引擎重復調用同一個能力門面實現批量請求的處理。下圖通過批量創意綁定接口給出了循環能力編排模式的使用示例,在該流程的最后一個環節通過registerFlatMapped能力編排API注冊了一個創意綁定能力(CreativeBindAbility)。該能力被設計為處理單個創意的綁定操作,而批量創意綁定領域服務卻需要在一次請求中完成多個創意的綁定操作,因此我們通過cmdFlatMap能力編排API定義了將領域服務參數中的待綁定創意列表展開成多個單創意綁定能力參數的規則,標準業務模版執行引擎將據此遍歷每一個單創意綁定參數并調用單創意綁定能力進而實現批量創意綁定。從示例中我們還可以看到,開發者在編排創意綁定能力時還通過transferCtxBeforeStep能力編排API指定在調用領域能力的上下文初始化和業務邏輯處理兩個標準步驟前都執行一次領域服務到領域能力的上下文傳遞操作,而標準業務模版執行引擎的默認行為是僅在領域能力上下文初始化標準步驟調用前執行該操作。除此之外,開發者還通過onFailure/onException能力編排API定制了失敗及異常處理措施,確保在循環過程中單個創意綁定失敗或異常不會中斷整個業務處理流程,而是在處理完所有待綁定創意之后,將綁定失敗的創意信息及失敗原因返回給調用方。

wKgZoWc7BJWAGJz0ABWPCC6Apns874.png

循環能力編排DSL示例

有些讀者可能會疑惑為什么不把創意綁定能力設計為直接在能力粒度上就支持批量綁定邏輯,這是因為當前的設計是綜合考慮能力劃分原則、創意綁定業務實際與拆分規則收益等因素之后決定的。我們曾在前文中提到,領域能力封裝的是最小原子業務模塊,而批量處理實際上屬于流程控制邏輯,因此從職責劃分的角度考慮,領域能力沉淀單個創意綁定的具體業務規則、領域服務負責循環流程控制的設計更符合PICASO框架的底層設計邏輯;另外從業務實際來分析,由于不同創意類型對應的創意綁定邏輯也有差異,因此創意綁定能力會按照創意類型拆分為不同的能力實例,而且廣告主有可能通過批量綁定接口一次性綁定類型不同的多個創意。在這樣的業務背景下,單創意綁定的能力拆分邏輯可以讓領域服務直接遍歷每一個待綁定創意,然后通過能力門面將單個創意綁定請求路由到與待綁定創意類型相適配的能力實例上進行處理,而不需要執行按創意類型分組等預處理邏輯;最后從拆分收益上看,單個創意綁定的拆分邏輯能夠給我們提供更多的編排靈活性,通過定制化的失敗及異常處理邏輯,不同的領域服務可以靈活地支持快速失敗或允許部分失敗等異常處理規則。綜上考慮我們最終采用了單創意綁定的能力拆分規則,而這個決策過程其實也是我們進行能力拆分的一般流程。從這個例子中我們可以發現,能力拆分其實是一個主觀性很強的行為,盡管我們為其制定了一系列的指導準則,但是在實際需求中開發者依然需要充分發揮主觀能動性,在充分理解新架構設計思想的前提下,緊貼業務實際,在系統性能、架構整潔程度以及實現成本之間找到最佳的平衡點。

截止到目前,PICASO領域能力編排框架已經經歷了兩輪功能迭代,前文所述的順序、條件及循環編排是第一代能力編排框架提供的特性,這一代能力編排框架僅支持領域能力之間編排串聯,而領域服務各個標準步驟內的業務邏輯和能力執行圖之間的串聯依然需要手動硬編碼實現。下圖給出了一個使用第一代能力編排框架的領域服務案例,這個案例也清晰地呈現了領域能力執行圖與領域服務各個標準步驟之間的關系。可以看到領域服務執行器提供了一個標準的領域能力編排入口registerDomainAbilities,該模板方法通過參數提供了一個領域能力編排器(DomainAbilityOrchestrator),開發者可以通過該模板方法完成領域能力執行圖的構建。然而當請求到來時,標準業務模版執行引擎并不會直接觸發領域能力執行圖的調用,它僅會按照標準業務執行流程依次調用領域服務執行器的各個標準步驟,而領域能力執行圖的調用則必須由開發者硬編碼到領域服務執行器的各個標準步驟中。當然框架已經將領域能力執行圖的觸發邏輯封裝成了相對易用的API(詳見下面截圖中doPreValidate方法中被標注的代碼片段),因此這種設計的實現成本不算太高。但是隨著系統中領域服務執行器數量的增長,它也的確在系統中引入了大量重復的膠水代碼,并且造成了領域服務中業務邏輯與領域能力值執行圖之間的割裂,無法提供徹底的連貫開發體驗。

wKgaoWc7BJiAVkOiAAcXGoy291o573.png

第一代能力編排框架依然存在一些問題

近年來,為了給廣告主提供簡潔易用的投放體驗,系統正越來越多地向著智能化和集成化的方向發展,讓廣告主少操作、少輸入成為UI交互設計重要原則。在這樣的業務背景下,我們收到了越來越多的跨層級接口合并需求,這讓我們的業務在模塊化的基礎上又呈現出集成化特征,而且很多情況下,這種集成是“跨層級”的。如過去我們的廣告物料一直遵循經典的計劃、單元、創意三層結構,計劃下可創建多個單元、單元下可綁定多個創,創建物料時需要依次調用三個接口。然而在近期全站推廣、一頁投放等需求都有一鍵創建全層級物料的訴求。但是由于子層級物料的創編流程都依賴父層級的物料對象創建完成,這與第一代能力編排框架中的重組執行機制底層邏輯相悖;另外,由于父層級物料對象與子層級物料對象之間都是一對多的關系,在一鍵創建全套物料的場景中,我們需要循環調用子層級物料的構建流程,然而第一代能力編排框架中循環能力編排特性僅支持循環調用單個領域能力,而不支持對多個領域能力執行鏈的重復觸發。

為了更好地適配業務發展趨勢,同時解決上文提到的第一代能力編排框架中由于領域服務與領域能力執行圖之間邏輯分離定義、需要手動硬編碼完成串聯造成的業務邏輯割裂、會引入大量重復的膠水代碼等問題,我們在第一代能力編排框架的基礎上進行了升級,引入了子流程編排機制。子流程編排最重要的升級是增加了分階段集成的特性,這也與當下多接口集成的業務發展趨勢相符,一個完整的請求處理流程可以被拆分為不同的執行階段,后置階段的執行邏輯可能依賴于前置階段的執行結果。PICASO框架允許開發者通過子流程編排DSL將一個完整的業務流程拆分定義為多個業務處理階段,其中每一個業務處理階段被稱為一個子流程,每一個子流程可以看做是一個小的領域服務,有各自專屬的標準處理步驟及領域能力執行圖。除了可以像第一代能力編排框架那樣為每一個子流程定義專屬的領域能力執行圖之外,第二代子流程編排框架還讓開發者不需要再去重寫領域服務執行器標準模板中的各個標準步驟,而是通過子流程編排DSL將每個子流程專屬的參數預校驗、上下文初始化、上下文校驗及業務邏輯處理四個標準步驟與領域能力執行圖一起直接定義到子流程執行圖中,從而徹底解決了第一代能力編排框架中領域服務維度的業務邏輯與能力執行圖開發割裂的問題。也正因為如此我們在第二代子流程編排DSL中將第一代編排框架中的領域能力編排器(DomainAbilityOrchestrator)升級成了領域服務構造器(DomainServiceBuilder)。

下圖給出了一個使用子流程編排DSL構建領域服務執行圖的例子,可以看到,與第一代能力編排框架類似,第二代子流程編排DSL為開發者提供了順序、條件、循環、捆綁及包裝五種子流程編排節點。其中循環子流編排節點支持子流程維度的循環調用,解決了第一代能力編排框架中FlatMap編排節點僅支持對單個能力進行循環調用的問題,因此在一對多的子層級物料創編場景中有廣泛的應用,如下面例子中的在單元下綁定關鍵詞列表的場景:一個單元下可以綁定多個關鍵詞,而單個關鍵詞的綁定操作需要串聯執行多個領域能力,此時我們可以將單個關鍵詞的構建過程定義為一個基礎順序子流程,然后再通過循環子流程編排節點將這個基礎子流程循環集成到單元創建領域服務執行圖中,這樣就可以實現在單元下批量綁定關鍵詞的功能;而捆綁子流程的作用類似于通用編程語言中的‘{}’,它能夠將多個子流程包裝成一個邏輯子流程節點,這一特性讓子流程編排能夠支持任意不同子流程之間的組合及嵌套,進一步提升了編排框架對各類業務場景的普適性;包裝子流程節點則是專門為已有領域服務執行器的組合復用而設計的,它可以將一個現有的領域服務執行器包裝成一個子流程編排節點添加到子流程編排執行圖中,這一特性在復用現有接口進行集成化改造或實現批量操作等需求中會為開發者提供極大的便利。

wKgZoWc7BJqAG4zPAAcvgL_PGlg969.png

第二代子流程及能力編排框架示例

通過上面的幾個例子我們能感受到,領域能力編排DSL能夠以極高的信息密度描述業務流程的關鍵信息,通過構建領域能力/子流程執行圖的方式定義業務流程的措施不僅能夠減少膠水代碼、降低開發負擔,還可以協助開發者快速建立起對業務的全景認知,同時,領域能力/子流程執行圖也能作為詳細業務規則的目錄或索引,在進行業務梳理或問題排查時讓開發者可以按圖索驥快速定位目標代碼的大致位置,提升業務知識傳承及問題定位的效率。

盡管我們在PICASO框架中舍棄了圖形化流程編排框架的設計,但是我們并沒有否定它存在的意義,這種編排方式在低代碼編程領域占有重要的地位。但是它更適合在一些節點類型有限或者流程相對穩定的業務場景下使用,比如審批流程,雖然審批流可能有很多,但是每個審批流程中的節點種類往往不多,可以用相對固定的模式進行串聯;或者廣告播放流程,盡管其業務流程同樣節點眾多、冗長復雜,但是流程數量相對固定,基本上可以分為展示、搜索、推薦、站外、合約這五大核心流程,很多變更是對流程的微調或節點內部的邏輯升級。與這些業務相比,廣告投放系統則維護了近300個能力節點、400多條請求處理流程,每次需求迭代都會涉及數十條業務流程的變更。在這樣的業務特點下,簡潔、靈活、集中、連貫的開發體驗要比一個炫酷的UI交互界面重要的多。當然,我們依然十分認可圖形化界面強大的呈現能力,事實上我們也在規劃為PICASO框架開發內置的代碼元數據管理平臺,其中一項重要的功能就是把代碼中基于能力編排DSL構建出來的領域能力執行圖解析為業務流程圖回顯到交互界面中,讓開發者可以直觀地查看所有領域服務的處理流程。但是該平臺只做能力編排邏輯的圖形化展示,領域能力執行圖的構造依然是通過代碼中的能力編排API實現的(當然該平臺也可以提供各個能力編排節點配置屬性的動態修改功能,但是這僅作為緊急情況下的應急處理措施)。

PICASO的愿景:構建圖書館式代碼架構

至此我們已經完成了對PICASO框架全部核心模塊的介紹,此刻讓我們再次回首PICASO框架的設次初衷——竭盡所能地提升團隊的研發效率,這也是一線業務開發團隊的核心價值所在。

提到研發效率,有很多同學認為少寫代碼就是研發效率高,也有同學認為支持了配置化、使用了拓展點就能實現研發效率的提升。然而事實上代碼行數從來就是不是制約研發效率提升的核心要素,對業務問題的抽象和封裝有時的確會導致我們多寫一些代碼,但是與多寫幾行代碼花費的幾十分鐘相比,在實際工作中我們會把更多的時間和精力消耗在確定分工時與合作團隊的扯皮上、需求設計時各團隊方案對齊及歷史業務邏輯的確認上、出現問題時原因的定位與影響范圍的評估上、由于自己或者上下游系統的技術或架構限制而不得不進行的妥協方案開發上......解決這些問題所花費的時間才是阻礙研發效率提升和耗盡業務方對研發團隊信任的根源。至于配置化和拓展點,不可否認的是它們的確是包括PICASO框架在內的很多效率提升解決方案中經常采用的措施,但是如果上述問題得不到解決,這些措施也只能是治標不治本。正如“鮑勃大叔”Robert C.Martin在其著作《架構整潔之道》中所說:“不管你多敬業、加多少班,在面對爛系統時你仍然會寸步難行,因為你大部分的精力是在應對混亂。”而我們之所以要為分工扯皮、之所以不敢升級系統以適配新的業態、之所以無法快速梳理出業務規則,都是因為過去大泥團式的代碼架構讓我們看不懂、不敢動、動不了。

為了解決上述問題,PICASO框架在設計之初就確立了框架即規范、代碼即文檔的基本原則,從設計階段就開始引導開發者以統一的思想和方法論對業務進行拆解分析,然后將業務規則淀到標準業務執行模版或拓展點接口中,確保每一處業務規則都有可檢索、可引用的實體邊界;接著通過能力編排框架以近乎白話文的方式將這些原子業務實體組裝起來,以極高的信息密度清晰地描述完整的業務流程;最后通過通用可執行實體發現及路由機制對各層實體進行分類及分組,對上層實體暴露統一的調用門面,除了起到逐層向上屏蔽分組內部場景復雜度的作用之外,PICASO框架維護的可執行實體路由表也可以作為業務細節邏輯的速查索引。有了這些措施,開發者進行業務邏輯梳理時就可以先通過領域服務維度的路由表定位到目標場景對應的領域服務實例,然后通過其領域能力執行圖快速建立起對務流程的全景認知,接著選擇執行圖中感興趣的業務節點,通過領域能力維度的路由表快速定位到具體的能力實例,最后到相應的的標準步驟中定位具體的實現細節。這種由粗及細、逐層按索引查找的過程類似于圖書館的管理模式:借閱圖書時,我們需要大體推斷目標書籍所屬的類目,然后通過類目確定書籍所在的書架,在書架上找到目標書籍后再通過其目錄快速概覽全書,最后通過目錄定位到感興趣的內容,這就是圖書館式代碼架構的由來。我們希望這種代碼架構能夠讓業務知識通過系統代碼清晰、準確、完整地表達出來并能流暢地傳承下去,進而讓團隊在面對業態更迭時能夠更加從容地承接業務方提出的各項訴求;在進行多團隊協作時能夠擁有足夠的底氣承擔更多的責任;在遇到線上問題時能夠更加快速地定位問題并制定解決方案;在接手新系統時也能夠快速梳理出業務主線并上手開發,最終實現團隊整體研發效率的提升。

(二)聚合與資源庫:拒絕魔法邏輯,讓代碼直接表達業務規則

如果說前文介紹的PICASO框架讓新架構擁有了領域驅動設計之形,那么接下來將要介紹的聚合與資源庫機制將讓新架構真正具備領域驅動設計的靈魂。

長期以來,我們的開發行為中業務設計與代碼設計是分離的。接到需求后研發人員會和產品及業務方進行大量的溝通,確認業務流程及各項細節規則,這是業務設計的過程。但是在編碼階段,開發人員又會對業務設計結果重新進行抽象,轉化為代碼實現方案。而此時傳統的三層架構過于簡單的層次劃分很容易將開發者的大部分注意力引導到數據庫設計上,代碼設計就變成了對數據庫增刪改查操作的設計。在這個過程中前期業務設計沉淀的大量領域知識往往會被丟棄,實現出來的代碼也失去了對業務規則的直接表達。而缺乏基礎模型設計的軟件充其量也只是一種機械化的產品,雖能實現功能卻無法解釋這樣操作的原因。更嚴重的是,如果底層數據庫存在表或字段的復用,那么業務規則被直接翻譯成庫表增刪改查操作邏輯之后,代碼甚至會表達出與業務規則完全不相符的含義出來。這些代碼就成了只有開發者自己才能看懂的魔法邏輯,甚至經過一段時間之后開發者本人也會忘記這些代碼背后真正的業務含義......

如果整個程序設計或者其核心部分沒有與領域模型相對應,那么該模型就是沒有價值的,軟件的正確性也值得懷疑。(Eric Evans, Domain-Driven Design: Trackling Complexity in the Heart of Software, 2003)

面向數據庫編程的設計思維還容易引導我們最終以事務腳本(Transaction Script)的形式實現業務流程的處理,將業務規則與數據庫表操作邏輯糅合到一起,業務實體之間的關系也因此分散和隱藏到了整個工程中不同的接口實現里,這會給數據模型全景認知的建立帶來極大的障礙。領域驅動設計思想的祖師爺及布道者Eric Evans曾提到自己項目組的成員曾花費數月時間才梳理出一個完整的數據模型,而在我們自己的記憶里,似乎也沒有哪位同學有底氣敢宣稱自己掌握了完整的數據模型(我們可能清楚數據庫中有哪些庫表,但是由于底層庫表存在不同業務場景復用的情況,導致表中數據對應的業務含義并不統一。這種復用表結構的設計無可厚非,在很多情況下我們甚至都鼓勵這種縱向拓展方式,但這也的確是造成我們對數據模型認識模糊和不完整的主要原因)。數據模型全景認知的缺失讓開發者很難進行統一的模型頂層設計,在多需求并行推進的開發模式下很容易在不同項目組之間形成信息孤島,無法實現模型復用與合并,導致數據模型野蠻膨脹,這反過來又進一步加劇了數據模型全景認知的構建難度,從而陷入到惡性循環中無法自拔。除此之外,業務流程中不同的業務環節可能會依賴相同的底層數據,事務腳本式的業務處理邏輯會將這些底層數據的讀寫操作分散到不同的接口或業務模塊的實現中,除了會造成重復編碼之外,還有可能在運行時造成重復和碎片化的數據讀寫操作,進而影響系統性能。

聚合與資源庫機制就是專門為解決上述問題而生的。

聚合與資源庫機制實現數據模型與業務邏輯分離

聚合(Aggregate)是領域驅動設計思想中重要概念,它是一組相關對象的集合,這些對象之間關聯密切,彼此之間已經按照對象之間的關系以父子屬性的方式組裝好。每個聚合都有明確的邊界(boundary)和一個聚合根(root),其中邊界定義了這個聚合中都有哪些對象,而聚合根則是這些對象中的一個特定實體。聚合根是聚合內唯一個允許被外部對象引用的實體,也是聚合中的所有實體的最頂層父級對象,因此通過聚合根可以訪問到聚合內所有對象,這會給上層業務規則的實現帶來了極大的便利。但是實際業務中聚合內的實體關系通常都十分復雜,常常存在多級嵌套關系。比如廣告投放業務中計劃聚合內就存在著“計劃->單元->創意->子創意->子創意審核記錄”這種多達5層的嵌套實體結構。如果每次使用聚合時都需要開發者手動實現聚合內實體的組裝邏輯顯然會帶來大量的重復代碼及編碼負擔,對系統未來的維護來說也將是一場災難,而資源庫就是為了解決這個問題而設計的。

資源庫(Repository)是系統中所有聚合根對象的邏輯集合,當然這并不意味著資源庫對象會直接加載和維護系統中所有的聚合根實例,它只是邏輯上的集合。事實上在實現層面業務數據始終保存在數據庫等存儲介質中,資源庫只是定義了針對聚合根或聚合根集合的增刪改查操接口,并且維護了底層存儲介質中的數據記錄與聚合實體對象之間的映射關系以及聚合中各個實體之間的關聯組合邏輯。因此開發者可以通過資源庫定義的標準接口一鍵獲取到一個組裝好的聚合根對象,就好像是從一個集合中取出一個元素那樣容易。如下圖所示,與傳統架構業務層代碼在不同業務環節中直接訪問數據庫的實現方式相比,新架構在上層業務(領域服務)與底層數據庫訪問層中間插入了一層資源庫,上層業務需要獲取聚合數據時,不需要自己實現聚合中各個業務實體的查詢、映射和組裝邏輯,而是通過資源庫提供的標準接口直接獲取已經組裝好的聚合根對象。這種設計將聚合內各個實體的查詢、映射及組裝邏輯收口屏蔽在資源庫內部,讓上層業務聚焦在業務規則上,從而實現了數據模型與業務邏輯的分離。這樣不僅能夠避免在業務邏輯中頻繁穿插繁瑣的數據查詢和組裝邏輯,防止在運行時出現重復及碎片化的數據讀寫操作,資源庫集中維護的各個聚合實體的查詢、映射和關聯邏輯也是一份重要的業務知識,能夠輔助開發者快速建立對完整數據模型的全景認知。

wKgZomc7BJuAYRM2AAQV10X_Rt8558.png

聚合與資源庫的引入實現了數據模型與上層業務模型的分離

六邊形架構讓代碼從業務中來到業務中去

在上一個小節我們介紹了聚合和資源庫的定義及實現準則,這其實有些本末倒置,因為聚合和資源庫機制的關鍵不是如何實現聚合實體的查詢或者持久化,而是如何設計一套有價值的聚合(當然,先了解聚合的實現準則對理解聚合的設計準則是有幫助的),而聚合的設計原則正是領域驅動設計思想核心理念的直接體現。

網絡上很多介紹DDD思想的文章都是從統一語言、領域、界限上下文等術語和概念開始的,但是由于中英文語境的差異和國內外軟件開發生態的不同,這些文章很容易讓初學者陷入到各種概念的泥沼中無法自拔。然而那些概念只是領域驅動設計思想的外在表現形式,其背后的思想內核才是我們應該優先掌握的內容。領域驅動設計思想的核心理念就是保持業務、領域模型和代碼三者之間的統一,而六邊形架構就是為了保障系統能夠達成這一目標而設計的,它的核心邏輯在于保護領域模型。

六邊形架構實際上是在分層架構的基礎上升級而來。領域驅動設計思想作為一種設計的指導思想其實并不會限制使用某種特定的架構,在傳統的分層架構上也可以實現領域驅動設計思想的落地。下圖中最左側給出了應用了領域驅動設計思想之后的分層架構,它看上去就是將傳統三層架構中的業務層拆分為應用層與領域層。其中領域層負責維護領域模型,所謂的領域模型就是由當前領域內的全部聚合、資源庫以及運行在這些聚合實體上領域能力和領域服務構成的;應用層則從業務視角定義了系統應該對外提供多少服務(也就是所謂用例(use case)和用戶故事(user story),如果你特別執著于那些術語的話),這些服務接口最終都會調用領域層中的領域服務執行器來實現接口功能;應用層關注的是在系統應該對外提供哪些服務,但是并不關心這些服務的請求來源是RPC調用還是MQ通知,這是用戶接口層的職責,它負責根據與調用方達成的約定將應用層接口暴露給不同的接口協議:Http、RPC、MQ、事件通知等等;基礎設施層則負責維護系統中使用的眾多中間件、工具以及底層存儲介質的訪問邏輯。

wKgaomc7BJyAEPELAAH8YarQCrE467.png

多層架構向六邊形架構的演進歷程

通過領域層和應用層的抽象,我們讓分層架構具備了實施領域驅動設計思想的可能。但是分層架構天然會引導開發者自上而下地以數據流的視角審視系統。而人腦的天性使我們更容易關注流程的始末,而容易輕視流程的中間環節。因此分層架構容易讓開發者更多的關注底層數據的存儲邏輯,進而再次陷入面向數據庫編程的思維,設計出與實際業務脫節的領域模型。當然,我們自然可以通過不斷的宣貫和世界觀輸出來呼吁開發者緊貼業務實際設計代碼實體,避免在需求伊始就陷入到底層庫表結構中無法自拔,但我們還是希望找到一種能夠在架構模式上就凸顯領域模型的重要性,引導開發者從業務實際出發設計領域模型的架構,這就是六邊形架構誕生的背景。在對六邊形架構進行詳細介紹之前,我們先回過頭來再次審視分層架構中用戶接口層和基礎設施層的職責差異,這對理解六邊形架構的本質十分重要:用戶接口層將系統服務暴露成不同的協議接口,因此其內部的代碼主要在執行接口參數轉換和應用層接口調用的邏輯;在領域服務返回處理結果之后,用戶接口層還需要將領域服務返回的響應結果包裝成符合對外接口協議的響應對象。而基礎設施層主要維護的是底層數據的訪問邏輯,似乎與用戶接口層的職責千差萬別。但是如果我們把數據庫看做是一個特殊的外部服務,基礎設施層的代碼執行邏輯就與用戶接口層幾乎一致了:基礎設施層負責的就是聚合實體與底層存儲PO對象之間的轉換及存儲介質數據傳輸協議的調用。我們不難發現用戶接口層與基礎設施層都在針對領域模型做防腐處理,從這個視角看,用戶接口層與基礎設施層在架構中的地位是相同的。因此,如上圖中間位置的架構圖所示,在架構模式上我們嘗試將用戶接口層與基礎設施層“掰”到一個同一個層級中合為一體,于是我們就能得到一個六邊形的對稱架構(從上圖的呈現方式上看,將用戶接口層與基礎設施層合并后可能還需要再旋轉45°才能呈現出與上圖右側一致的六邊形架構)。

六邊形架構本質上還是一個分層架構,只是在呈現方式上(注意不是實現方式)將用戶接口層與基礎設施層合二為一,讓他們共同作為防腐層保護位于架構中心的領域模型不被調用方的請求協議以及底層數據庫的特殊設計所污染。而人腦天然具備的找中心的特性能夠讓開發者將更多的注意力放到位于架構中心的領域模型上,暫時忘記底層數據庫的存儲規則,進而能夠緊貼業務實際設計聚合中的各個實體及值對象,讓代碼直接表達業務規則,最后通過資源庫實現聚合實體與底層存儲介質PO對象之間的轉化。

下圖給出了一個廣告投放業務中聚合實體與底層庫表結構設計的例子,投放系統作為一個業務集成平臺需要不斷地對接各種垂直業務系統,在廣告物料中也需要不斷集成各類業務實體,這些數據也需要保存到底層的廣告物料數據中。在設計單元聚合時,我們會緊貼業務實際為各類外部關聯對象定義含義明確的聚合實體:商品庫(ProductCategory)、抖音賬號信息(AweneAccount)、展示錨點信息(AnchorInfo)、直播信息(LiveInfo)等,從而確保領域模型與實際業務的統一。但是在設計底層庫表結構時為了避免庫表結構膨脹以及表字段稀疏化,我們采用了通用化的存儲結構:綁定到廣告物料上的不同外部業務對象都保存到同一張外部關聯對象表中,該表中的字段采用泛化設計,不與任何一種特定的外部業務對象相綁定,而是通過type字段確定本條記錄對應的外部對象類型以及表中其他字段的實際業務含義,業務層對這些外部業務對象讀寫操作都要通過資源庫集中維護的底層數據記錄與領域層聚合實體之間的映射邏輯做轉換。需要特別說明的是,在新架構中我們將PICASO框架中的拓展點機制延伸到了基礎設施層,引入了模型管理拓展點(Model Manage Extension, MME)來實現聚合組裝主流程與底層數據對象轉換邏輯的解耦。在本例中,我們將外部關聯對象表對應的PO對象與領域層聚合實體之間的映射邏輯抽離成了一個模型管理拓展點,以外部關聯對象類型作為路由鍵,通過PICASO框架的通用可執行實體發現與路由機制實現具體拓展點實現的自動定位??梢钥闯鲑Y源庫的存在不僅讓開發者可以聚焦業務實際設計領域模型,讓代碼直接反映業務實際,同時還讓通用化的底層數據存儲結構得到更加廣泛的應:庫表結構的通用化設計雖然能夠解決數據庫模型膨脹的問題,但是也的確降低了數據模型對業務的表達能力,而且如果讓上層業務直接操作這些庫表,勢必會造成代碼邏輯不明、編碼繁瑣的問題。然而資源庫卻通過收口底層數據對象與上層業務實體之間的轉化邏輯很好地解決了這些問題,讓我們在采納這種通用化的數據存儲結構設計時少了很多顧慮。

wKgZomc7BKCACZ7nAAJtThWOwI0104.png

底層通用化存儲結構(K-V模式)在資源庫中通過模型管理拓展點被映射為領域模型中業務含義明確的實體對象

在框架實現層面六邊形架構與分層架構其實并沒有太多的差異,頂多就是在六邊形中領域模型僅負責定義資源庫接口,而將資源庫的實現放到了基礎設施層中。在使用了Spring等控制反轉容器的項目中,一些宣稱采用了分層架構的系統可能已經在無意間實現了六邊形架構了。那么六邊形架構的意義又是什么呢?我們在前文中曾引用了IEEE對“架構”的定義:組織、組件以及指導思想,然而很多時候我們都忽略了指導思想對軟件開發行為的重要影響。一個好的架構一定是包涵人性的,架構思想直接決定了開發者分析業務的世界觀和方法論??蚣苤皇禽o助工具,基于框架思想對業務進行抽象和設計而產出的代碼才是一個軟件系統的主要構成部分。即使采用相同的框架,在不同架構思想的引導之下,系統中的業務代碼也可能會走向全然不同的迭代路線。而六邊形架構與分層架構的差異正體現在二者對開發行為的指導思想上,六邊形架構以領域模型為中心,引導開發者始終緊貼業務實際進行模型設計。它與PICASO框架相輔相成,讓系統在應對高復雜度業務時依然能保持對業務規則的清晰表達。

聲明式數據操作既要規范又要靈活

如前文所述,一個聚合會包含業務子域內的全部實體與值對象,資源庫會維護這些業務實體的查詢、映射及組裝邏輯。但是并不是每一個領域服務都會用到聚合中的全部實體,如果每次獲取聚合根時都將聚合內所有實體都查詢出來,勢必會造成極大性能損耗。然而資源庫作為基礎設施層中的底層組件,也不可能為每一個領域服務提供專用的聚合查詢或者數據持久化接口。為了調和這兩者之間的矛盾,我們在資源庫實現上采用了聲明式數據操作設計。

在《能力編排領域特定語言》章節中我們已經對聲明式編程有了比較具象的體會,但聲明式編程并不限定于領域特定語言這一種實現形式,在資源庫的接口設計上我們采用了一種更加樸素的聲明式編程實現方法。如下圖所示,我們計劃聚合的查詢以及創意聚合的更新接口為例來闡述聲明式數據操作的設計細節。當需要從資源庫中獲取聚合根對象時,默認情況下資源庫庫會自動查詢聚合下所有子實體并完成組裝,但是開發者可以再提供一個額外的輔助查詢參數DomainQuery,并通過該對象的addSubEntityQuery方法聲明希望獲取哪些特定的子實體。默認情況下資源庫會根據聚合根對應的主表記錄ID做子實體查詢,如果開發者希望在子實體查詢時使用額外的匹配條件,則可以通過addSubEntityQuery方法同時聲明希望獲取的子實體類型以及執行該類型子實體查詢時使用的額外匹配參數,這一特性在與聚合根存在一對多的子實體查詢場景中會發揮重要作用。上述聲明式接口在資源庫實現中并不復雜,只需要在執行每個子實體的查詢或修改操作之前,判斷一下上層調用方是否限定了僅對部分子實體進行操作,如果設置了則進一步判斷當前子實體是否在上層調用方指定的子實體范圍之內,如果當前子實體業務上允許調用方指定自定義的匹配邏輯,還需要嘗試從輔助查詢/修改參數中提取調用方為該子實體指定的自定義匹配邏輯。需要特別說明的是,我們不推薦在新架構內部各個模塊之間采用任何形式的黑盒調用模式,不管是能力編排還是資源庫調用,上層調用方都有責任和義務理清底層模塊的內部執行邏輯,確保編排配置或調用參數設置正確。

wKgaomc7BKGAOfQ5AAWhkCGo63w963.png

基于資源庫進行聲明式聚合數據查詢

四、新架構下的業務開發流程速覽

業務建模

新架構作為領域驅動設計思想的戰術落地框架,能夠讓其發揮出最大價值的前提是對業務進行良好的領域建模,下圖給出了廣告投放平臺中競價及合約廣告投放服務的業務架構。由于本文的主題聚焦在如何腳踏實地地實現一個基于領域驅動設計思想而設計的系統,因此關于DDD思想戰略設計相關的內容本文將不做過多闡述,相關內容我們將在后續《領域驅動設計與PICASO框架》一文中進行詳細闡述。需要說明的是,圖中的聚合服務層、領域能力層及數據模型層共同構成了廣告投放平臺和核心領域模型。

wKgZomc7BKOAcvHsAArWhlqeZiw375.png

競價及合約廣告投放服務分層業務架構

工程結構

下圖給出了新架構思想落地時工程結構的最佳實踐案例,工程中各module的職責以及與上文中業務分層架構圖中各層的對應關系依次為:

rtbad-framework:框架包,承載架構標準規約及分層架構圖中基礎設施層中的各項基礎組件。

rtbad-module/rtbad-support-module:模型包,對應的是分層架構圖中的模型層,承載領域模型中各個聚合及聚合實體對象的定義,另外用于實現底層存儲介質中持久化數據與領域模型中實體對象映射邏輯的資源庫也定義在此類module中。其中support-module定義的是支撐域中的實體對象,該module下會按照業務子域進一步進行子module劃分。

rtbad-event:事件包,屬于特殊的數據模型層,承載這領域事件對象的定義,新架構底層融合了事件驅動架構(篇幅的關系我們會在其他文章中進行專門地介紹),因此事件體系建設也被納入到統一建模的工作中。

rtbad-composite/rtbad-support-compoaite:聚合服務包,對應分層架構中的領域能力及聚合服務層,承載領域能力及領域服務執行器的實現,其中support-Composite用于承載支撐域領域能力及領域服務執行器的實現,該module下會按照業務子域進一步進行子module劃分。

rtbad-app:部署層,內部分為不同的子module,每一個子module對應一個部署應用,其實現邏輯就是根據應用職責組裝底層各個子module,進而實現不同應用下能力及模型共享。

rtbad-api:對外接口SDK包,承載了對外提供的API接口定義。

wKgaomc7BKWASYayAAaI6h5NmFQ064.png

代碼開發流程示例

以下內容以一次計劃新建請求為例,通過從流量入口到數據落庫的完整請求流程展示使用新架構實現業務需求的全部過程,我們希望通過這個例子讓大家建立對新架構的直觀感受。

領域服務統一入口

領域服務統一入口(Domain Service Faced)的作用是為HTTP、RPC、MQ等上層不同的請求流量暴露統一的服務入口。他將同一個業務子域內領域服務執行器集中到一起,便于流量介入層調用,同時也可以集中進行方法性能監控、調用量統計、請求日志記錄等通用功能。

wKgZomc7BKaABFDeAAJdCw-cgPo681.png

領域服務門面執行器

領域服務統一入口引用的是各個領域服務門面執行器,它負責從參數中提取業務標識并定位到具體的領域服務實例。如下圖所示,所有具體的領域服務實例都繼承自領域服務門面執行器,請求到來時領域服務門面先通過generateRouteKey方法從當前參數中提取出本次請求的業務標識,然后與各個領域服務實例能夠支持的業務標識做匹配,從而定位到應該處理本次請求的服務實例。

wKgaomc7BKeAURU4AAPIDEJWJeA994.png

領域服務實例執行器

能力執行圖的作用是定義業務執行流程,框架提供了豐富的API及大量的語法糖和默認規則,配合鏈式調用的風格,在支持靈活編排的同時減少了開發者的負擔。

wKgZomc7BKuARYsFAB9Nis5QbY0534.png

領域能力門面執行器

負責從參數中提取場景標識并定位具體的領域能力實例。

wKgaomc7BKyAGQZGAAI1IMWlhzQ653.png

領域能力實例執行器

能力內主要負責實現具體的業務規則,并對聚合根中相關屬性進行設置/修改,一個領域服務編排的所有領域能力執行完成之后,就能獲取一個完整的、全新的聚合根對象。

wKgZomc7BK2AdRd4AAEYXbj3iwU805.png

拓展點

拓展點的作用是作為任意維度的差異點補充分離工具。需要注意的是在新架構中拓展點不是唯一的差異分離工具,在通用對象發現及路由機制下,領域服務、領域能力和拓展點都在不同維度上起著差異點分離的作用。相對與前兩者拓展點更加靈活,通常用來承接領域服務及能力自身路由維度之外的邏輯差異。比如出價設置能力已經按照出價方式做了能力維度的差異分離,但是在相同的出價方式下,京準通與流量貨幣化還存在一些細微的邏輯差異,那么這個時候就可以在該能力實例中通過拓展點來補充實現平臺維度的差異邏輯分離。

wKgaomc7BK-Aed3VAAJBxg6hF6c374.png

上面的例子是在計劃名稱設置能力中獲取不同產品線計劃名稱長度上下限配置的拓展點,計劃名稱設置能力自身按照產品線類型進行業務模式路由,但是站內計劃名稱設置主要業務邏輯基本一致,僅在部分校驗邏輯上不同產品線有各自的要求,此時就可以使用一個名稱設置能力實例服務所有的站內產品線,定義名稱設置主體業務規則,而把不同產品線的細微差異抽象成一個拓展點接口。

資源庫

上層業務通過聲明式接口實現聚合實體讀寫操作。

wKgZomc7BLCAb0g1AAIybkGeybI247.png

wKgaomc7BLGAKR8WAAHSu84Tghg241.png


五、結語

很多同學對業務開發一直存在一種偏見,認為業務開發很簡單,甚至有業務開發同學自己也時常調侃自己是CRUD工程師,認為自己的工作沒什么技術含量。但其實業務開發一點都不簡單,只是過去我們一直把它做簡單了。如今業務形態復雜多變,商機轉瞬即逝,如何在快速變化著的復雜業務需求中維持系統健康、穩定、持續迭代,要做到這一點的難度其實一點都不比底層技術差。程序員應該是一門充滿學術性與創造性的職業,我們唯有堅守初心,不斷夯實自己的技術功底,沉淀提升抽象與建模能力,培養自己的系統化思維,不斷學習精進,追求極致編碼,這才是我們無法被AI替代的核心競爭力與價值所在。

有同學可能會關注本文介紹PICASO框架未來是否可以對外共享,關于這個問題我們的答案是肯定的。在PICASO框架開發之初我們的野心就沒有局限在京東廣告投放平臺這一個業務場景上,而是希望它可以走出廣告部,甚至走出京東,接受全社會開發者的檢驗,成為一個被業界認可的復雜B端業務通用解決方案。然而作為一個一線業務團隊,快速支持業務方需求是我們的首要職責。盡管我們在進行PICASO底層框架開發時盡力維持與具體業務分離的開發原則,但是在需求排期比較緊張的時候,為了快速支持業務需求的開發,還是存在將與廣告投放業務相關的邏輯耦合到了PICASO框架底層源碼中的情況。如果大家有興趣閱讀或者試玩PICASO的源碼,請聯系筆者為您開放一個示例版本的框架源碼權限,該版本框架的功能與筆者負責的線上系統使用的框架功能完全相同,只是去除了廣告投放業務相關的邏輯。您可以在該版本源碼上執行任意的功能及性能測試,但是在我們對外發布正式的共享版本之前,我們并不建議您直接將該示例版本的源碼應用到線上系統。目前框架功能已趨于穩定,我們也將PICASO框架的開源化改造提上日程,也歡迎感興趣或者有開源社區維護經驗的同學一起交流共建。

審核編輯 黃宇

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 數據模型
    +關注

    關注

    0

    文章

    49

    瀏覽量

    10001
  • 代碼
    +關注

    關注

    30

    文章

    4780

    瀏覽量

    68539
  • 架構
    +關注

    關注

    1

    文章

    513

    瀏覽量

    25468
收藏 人收藏

    評論

    相關推薦

    京東廣告投放平臺整潔架構演進之路

    設計思想到落地框架都進行了徹底的革新,涉及內容比較多,因此我們希望通過一系列文章循序漸進地闡述本次架構升級的始末。新架構并不是一日而成的,而是經過了多次架構升級的演進,因此我們將本文作
    的頭像 發表于 09-18 10:26 ?843次閱讀
    京東廣告投放平臺<b class='flag-5'>整潔</b><b class='flag-5'>架構</b>演進之路

    【「大模型時代的基礎架構」閱讀體驗】+ 未知領域的感受

    再到大模型云平臺的構建,此書都有提及和講解,循序漸進,讓讀者可以由點及面,由面到體的來認識大數據模型的體系架構。 前言中,作者通過提出幾個問題來引導讀者閱讀思考——分布式AI計算依賴哪些硬件特性
    發表于 10-08 10:40

    大學生循序漸進的學習歷程如何開啟

    本帖最后由 gk320830 于 2015-3-9 01:50 編輯 作為一個本科電子專業大二的學生,剛接觸專業課的我,對于如何更好的掌握專業知識,同時從更多的渠道豐富自己感到有些茫然。剛接觸電子專業知識感到十分生澀。如何開啟我大學的電子之路,更好借鑒大家的經驗,達到博而精的效果請教各位前輩。我會時刻駐足論壇,誠懇接受諸位的建議!謝謝!
    發表于 09-29 20:20

    《鴻蒙設備學習菜鳥指南》之 【五、搭建開發環境】

    ,然后再配置復雜的方案,循序漸進。最簡化方案:? Windows系統:安裝VSCode就好了,就是這么簡單? MacOS系統:同上? Linux系統
    發表于 10-30 13:59

    如何系統的學習嵌入式?

    都說嵌入式很難,即使去嵌入式培訓機構做系統訓練,其實只是沒有掌握正確的學習嵌入式的方法,學習講究的是一個循序漸進的過程,誰也不能一口吃出一個大胖子,從基礎到專業,從簡單到高深,下面達內講解一下系統學習嵌入式培訓的基本步驟:
    發表于 03-09 06:23

    【電子書】Linux命令速查手冊PDF

    `本書以目前最熱門的Linux發行版——Ubuntu為平臺,從零開始循序漸進地介紹了Linux系統的基礎知識和實用操作。`
    發表于 04-02 14:05

    Linux嵌入式學習過程 精選資料推薦

    Linux嵌入式學習過程循序漸進學習嵌入式開發技術一、練好基本功二、嵌入式Linux應用開發誤區一、全身投入學習桌面或服務器版本linux系統誤區二、直接閱讀linux內核源代碼如何正確的嵌入式
    發表于 07-19 09:07

    VB6循序漸進教程

    VB6循序漸進教程
    發表于 02-06 16:52 ?50次下載

    操作系統原理及應用(linux)_王紅

    本書在內容編排上力求由淺入深,循序漸進,舉一反三,突出重點,通俗易懂。采用模塊化結構,兼顧不同層次的需求。
    發表于 12-12 14:58 ?0次下載
    操作<b class='flag-5'>系統</b>原理及應用(linux)_王紅

    PERL編程24學時教程(完整版)

    perl語言的學習資料,由淺入深。循序漸進
    發表于 11-17 10:21 ?0次下載

    Linux系統的保護,四個步驟循序漸進有奇效

    如今,互連設備比以往更易受到安全威脅,但是錯綜復雜的平臺讓開發人員很難顧及到每一個潛在的入口點。為幫助減少基于 Linux 的系統所遭受到的威脅,英特爾?公司旗下的 Wind River 提出了 OEM 應該遵循的四步流程:監控、評估、通知和修復。
    發表于 09-11 16:32 ?8次下載
    Linux<b class='flag-5'>系統</b>的保護,四個步驟<b class='flag-5'>循序漸進</b>有奇效

    2021 年問世!蘋果自研 5G 芯片

    預計將先從 iPad 等「次級」產品循序漸進采用自家芯片。
    的頭像 發表于 08-01 17:48 ?2862次閱讀

    人工智能在發展的路上怎樣避免陷阱

    為了更好的推動其發展,人工智能的落地與應用必然會是一個循序漸進的過程。
    發表于 11-12 14:31 ?567次閱讀

    電力電子技術從基礎到運用

    想學習電力電子技術的 這本書可以啊 從簡單到基礎 循序漸進
    發表于 03-11 14:54 ?0次下載

    高級進階:復雜業務系統的通用架構設計

    兩點的規模和復雜性直接決定了系統復雜程度。比如就拿我們的電商系統舉例,分成很多部分,商品、庫存、采購、訂單、物流、財務,這個只是大的分類,還有針對 C
    的頭像 發表于 08-14 11:33 ?606次閱讀
    高級進階:<b class='flag-5'>復雜</b>業務<b class='flag-5'>系統</b>的通用<b class='flag-5'>架構</b>設計
    主站蜘蛛池模板: 99视频这里只有精品| 天天久久狠狠色综合| 婷婷色色狠狠爱| 亚洲精品无码不卡在线播HE| 91久久综合精品国产丝袜长腿| 99热在线精品免费全部my| 搞基福利社| 男男高H啪肉Np文多攻多一受| 天天影视网网色色欲| 最新精品学生国产自在现拍| 东京热百度影音| 男女做爽爽爽视频免费软件| 亚洲伊人精品| 国产麻豆AV伦| 日本乱hd高清videos| 中文文字幕文字幕亚洲色| 黄小说免费看| 午夜色网站| 国产传媒在线播放| 欧美精品成人a多人在线观看| 一个人在线观看免费中文www| 国产欧美日韩中文视频在线| 色欲AV亚洲午夜精品无码| 豆奶视频在线高清观看| 神电影院午夜dy888我不卡| 超碰在线视频人人AV| 人妻美妇疯狂迎合| 俄罗斯美女z0z0z0在线| 天堂网久久| 调教美丽的白丝袜麻麻视频| 啊…嗯啊好深男男小黄文| 欧美午夜精品久久久久久浪潮| 亚洲 综合 自拍 精品 在线| 把腿张开老子CAO烂你动态图| 起碰免费公开97在线视频| 国产99久久久欧美黑人刘玥| 亞洲人妻AV無碼在線視頻| 两个人看的www免费高清直播| bbbbbxxxxx肥胖| 午夜福利92看看电影80| 久久精品一本到99热|