阿里高級技術專家:整潔的應用架構“長”什么樣?
作者張建飛是阿里巴巴高級技術專家,入司6年,他創建了COLA。希望可以探索一套切實可行的應用架構規范,這個規范不是高高在上的紙上談兵,而是可以復制、可以理解、可以落地、可以控制復雜性的指導和約束。本文詳述了他對COLA的升級迭代。
很多同學不止一次和我反饋,我們的系統很混亂,主要表現在:
- 應用的層次結構混亂:不知道應用應該如何分層、應該包含哪些組件、組件之間的關系是什么;
- 缺少規范的指導和約束:新加一段業務邏輯不知道放在什么地方(哪個類,哪個包)、應該起什么名字比較合適?
解決這些問題,正是我創建COLA(https://github.com/alibaba/COLA)的初心之一——試圖探索一套切實可行的應用架構規范,這個規范不是高高在上的紙上談兵,而是可以復制、可以理解、可以落地、可以控制復雜性的指導和約束。
自從COLA誕生以來,我收到了很多的意見和建議。同時,我自己在實踐過程中,也發現COLA 1.0的諸多不足,有些設計是冗余的并不是很有必要,而有些關鍵要素并沒有囊括。譬如,我最近在思考的應用架構核心和復雜業務代碼治理就是對COLA 1.0的反思。
結合實踐中的探索和對復雜度治理持續的思考,我決定對COLA進行一次全面的升級,于是有了現在的COLA 2.0。
從1.0到2.0,不僅僅是數字的簡單變化,更是架構理念和設計理念的升級,其主要變動點包括:
- 新架構分層:Domain層不再直接依賴Infrastructure層。
- 新組件劃分:對組件進行了重新定義和劃分,加了新組件,去除了一些老組件(Validator,Convertor等)。
- 新擴展點設計:引入了新概念,讓擴展更加靈活。
- 新二方庫定位:二方庫不僅僅是DTO,也是Domain Model的輕量級表達和實現。
新架構分層
在COLA 1.0中,我們的分層是如下圖所示的經典分層結構:
在COLA 2.0中,還是這些層次,但是依賴關系發生了變化,Domain層不再直接依賴Infrastructure層,而是引入了一個Gateway的概念,使用DIP(Dependency Inversion Principle,依賴倒置)反轉了Domain層和Infrastructure層的依賴關系,其關系如下圖所示:
這樣做的好處是Domain層會變得更加純粹,完全擺脫了對技術細節(以及技術細節帶來的復雜度)的依賴,只需要安心處理業務邏輯就好了。
除此之外,還有兩個好處:
1. 并行開發:只要在Domain和Infrastructure之間約定好接口,可以有兩個同學并行編寫Domain和Infrastructure的代碼。2. 可測試性:沒有任何依賴的Domain里面都是POJO的類,單元測試將會變得非常方便,也非常適合TDD的開發。
新組件劃分
模塊和組件的定義
首先,先明確一下組件(Component)這個概念的定義,組件在Java中(或者說在本文中),其范圍就是Java的包(Package)。
還有一個詞叫模塊(Module),組件和模塊這兩個概念是比較容易發生混淆的。比如在《實現領域驅動設計》中,作者就說:
If you are using Java or C#, you are already familiar with Modules, though you know them by another name. Java calls them packages. C# calls them namespaces.
他認為Module是Package,我認為這個定義容易造成混淆。特別是在使用Maven的時候,在Maven中,Module是一個Artifact,通常是一個Jar而不是Package。比如COLA Framework就包括如下四個Module:
- <modules>
- <module>cola-common</module>
- <module>cola-core</module>
- <module>cola-extension</module>
- <module>cola-test</module>
- </modules>
的確,Module和Component這兩個概念很相近,很容易造成混淆。比如,在StackOverflow上有一個提問【1】,就是問Module和Component之間區別的。獲得最高贊的答案是通過Scope來區分的。
The terms are similar. I generally think of a "module" as being larger than a "component". A component is a single part, usually relatively small in scope.
這個回答和我的直覺反應是一致的,即Module比Component要大。根據以上信息,我在此對Module和Component進行一下定義說明,在本文中,都會遵照如下的定義和Notation(表示法)。
- 模塊(Module):和Maven中Module定義保持一致,簡單理解就是Jar。用正方體表示。
- 組件(Component):和UML中的定義類似,簡單理解就是Package。用UML的組件圖表示。
一個Moudle通常是由多個Component組成的,其關系和表示法如下圖所示:
COLA 2.0的組件
在COLA 2.0中,我們重新設計了組件,引入了一些新的組件,也去除了一些舊組件。這些變動的宗旨是為了讓應用結構更加清晰,組件的職責更加明確,從而更好的提供開發指導和約束。
新的組件結構如下圖所示:
這些組件各自都有自己的職責范圍,組件的職責是COLA的重要組成部分,也就是我們上面說的“指導和約束”。這些組件的詳細職責描述如下:
二方庫里的組件:
- api:存放的是應用對外的接口。
- dto.domainmodel:用來做數據傳輸的輕量級領域對象。
- dto.domainevent: 用來做數據傳輸的領域事件。
Application里的組件:
- service:接口實現的facade,沒有業務邏輯,可以包含對不同終端的adapter。
- eventhandler:處理領域事件,包括本域的和外域的。
- executor:用來處理命令(Command)和查詢(Query),對復雜業務,可以包含Phase和Step。
- interceptor: COLA提供的對所有請求的AOP處理機制。
Domain里的組件:
- domain:領域實體,允許繼承domainmodel。
- domainservice: 領域服務,用來提供更粗粒度的領域能力。
- gateway:對外依賴的網關接口,包括存儲、RPC、Search等。
Infrastructure里的組件:
- config:配置信息相關。
- message:消息處理相關。
- repository:存儲相關,是gateway的特化,主要用來做本域的數據CRUD操作。
- gateway:對外依賴的網關接口(Domain里的gateway)的實現。
在使用COLA的時候,請盡量按照組件規范約束去構建我們的應用。這樣可以讓我們的應用結構清晰、有章可循。如此這般,代碼的可維護性和可理解性會得到極大的提升。
新擴展點設計
引入新概念
在討論之前,我們先來明確一下在COLA2.0擴展設計中引入的新概念:業務、用例、場景。
- 業務(Business):就是一個自負盈虧的財務主體,比如tmall、淘寶和零售通就是三個不同的業務。
- 用例(Use Case):描述了用戶和系統之間的互動,每個用例提供了一個或多個場景。比如,支付訂單就是一個典型的用例。
- 場景(Scenario):場景也被稱為用例的實例(Instance),包括用例所有的可能情況(正常的和異常的)。比如對于“訂單支付”這個用例,就有“可以使用花唄”,“支付寶余額不足”,“銀行賬戶余額不足”等多個場景。
簡單來說,就是一個業務是有多個用例組成的,一個用例是有多個場景組成的。用淘寶做一個簡單示例,業務、用例和場景的關系如下:
新擴展點的實現
在COLA 2.0中,擴展的實現機制沒有變化,主要變化就在于上文中引入的新概念。因為COLA 1.0的擴展設計思想來自于星環,所以當初的擴展粒度也是copy了星環的“業務身份”。COLA 1.0的擴展定位的方法如下圖所示:
然而,在實際工作中,能像星環那樣支撐多個業務的場景并不常見。更多是對不用用例,或是對同一個用例不同場景的差異化支持。比如“創建商品”和“更新商品”是兩個用例,但是大部分的業務代碼是可以復用的,只有一小部分需要差異化處理。
為了支持這種更細粒度的擴展支持,除了之前的“業務身份(BizId)”之外,我還引入了Use Case和Scenario這兩個概念。新的擴展定位如下圖所示:
可以看到,在新的擴展框架下,原來只能支持到“業務身份”的擴展,現在可以支持到“業務身份”,“用例”,“場景”的三級擴展,無疑比以前要靈活的多,并且在表達和可理解性上也比以前好。
在新的擴展框架下,例如我們實現上圖中所展示的擴展:在tmall這個業務下——的下單用例——的88VIP場景——的用戶身份校驗進行擴展,我們只需要聲明一個如下的擴展實現(Extension)就可以了。
新二方庫定位
關于二方庫的定位表面上來看,是一個簡單問題,因為服務的二方庫無外乎就是用來暴露接口和傳遞數據的(DTO)。不過,往深層次思考,它并不是一個簡單的問題,因為它涉及到不同界限上下文(Bounded Context)之間的協作問題。 它是分布式環境下,不同服務(SOA,RPC,微服務,叫法不同,本質一樣)之間如何協作的重要架構設計問題。
Bounded Context之間的協作
如何實現不同域之間的協作,同時又要保證各自領域的概念的完整性是有一套方法論的。總體來說,大概有兩種方式:共享內核(Shared Kernel)和防腐層(ACL,Anti-Corruption Layer)。
1. 共享內核(Shared Kernel)
It’s possible that only one of the teams will maintain the code, build, and test for what is shared. A Shared Kernel is often very difficult to conceive in the first place, and difficult to maintain, because you must have open communication between teams and constant agreement on what constitutes the model to be shared.
上面是引用《DDD Distilled》(作者是Vaughn Vernon)關于Shared Kernel描述的原話,其優點是Share(減少重復建設),其缺點也是Share(團隊之間緊耦合)。
2. 防腐層(ACL,Anti-Corruption Layer)
An Anticorruption Layer is the most defensive Context Mapping relationship, where the downstream team creates a translation layer between its Ubiquitous Language (model) and the Ubiquitous Language (model) that is upstream to it.
同樣是來自于《DDD Distilled》, 防腐層是隔離最徹底的做法,其優點是沒有Share(完全解耦,各自獨立),其缺點也是沒有Share(有一定的轉換成本)。
不過我和Vernon的觀點差不多,都比較贊成防腐層的做法。因為增加的語義轉換陳本,相較于系統的可維護性和可理解性而言,是完全值得的。
Whenever possible, you should try to create an Anticorruption Layer between your downstream model and an upstream integration model, so that you can produce model concepts on your side of the integration that specifically fit your business needs and that keep you completely isolated from foreign concepts.
二方庫的重新定位
在大部分情況下,二方庫的確是用來定義服務接口和數據協議的。但是二方庫區別于JSON的地方是它不僅僅是協議,它還是一個Java對象,一個Jar包。
既然是Java對象,就意味著我們就有可能讓DTO承載除了getter,setter之外的更多職能。這個問題以前沒有引起我的重視,但是最近在思考domain model的時候,我發現,我們是可以在讓二方庫承擔更多職責的,發揮更大的作用。
實際上,在阿里,我發現有些團隊已經在這么實踐了,而且我覺得效果還不錯。比如,中臺的類目二方庫,在這個事情上就做了比較好的示范。類目是商品中比較復雜的邏輯,里面涉及很多計算,我們先看一下類目二方庫的代碼是怎么寫的:
從上面的代碼,我們可以發現這已經遠遠超出DTO的范疇了,這就是一個Domain Model(有數據,有行為,有繼承)。這樣做合適嗎?我認為是合適的:
- 首先,DefaultStdCategoryDO用到的所有數據都是自恰的,即這些計算是不需要借助外面的輔助,自己就能完成。比如判斷是否是根類目,是否是葉子類目,獲取類目的名稱路徑等,都是依靠自己就能完成。
- 其次,這就是一種共享內核,我把自己領域的知識(語言、數據和行為)通過二方庫暴露出去了,假如有100個應用需要使用isRoot( )做判斷,你們都不需要自己實現了。
什么?不是說不推薦共享內核的做法嗎?(好吧,小孩子才分對錯,好嗎)。此處的共享內核我認為是有積極意義的,特別是類目這種輕數據、重計算的場景。不過,共享帶來的緊耦合也的確是一個問題。所以如果我是類目服務的Consumer的話,我會選擇用一個Wrapper去對Category進行包裝復用,這樣既可以復用它的領域能力,又可以起到隔離防腐的作用。
COLA中的二方庫
說到這里,我想你應該已經理解我對二方庫的態度了。是的,二方庫不應該僅僅是接口和DTO,而是領域的重要組成部分,是實現Shared Kernel的重要手段。
因此,我打算在COLA 2.0中擴大二方庫的職責范圍。主要包括兩點:
二方庫中的domain model也是領域的重要組成部分,是“輕量級”的領域能力表達,所謂“輕量級”是說表達是自恰和足夠內聚的,類似于上面說的StdCategoryDO的案例。當然,能力的表達也需要遵循通用語言(Ubiquitous Language)。
不同Bounded Context之間的協作,要充分利用好二方庫的橋梁作用。其協作方式如下圖所示。
注意,這只是建議,不是標準。實際上,我們永遠要在共享和耦合之間做一個權衡,世界上沒有完美的架構,也沒有完美的設計。 合不合適,還需要你自己根據實際場景自己去定奪。
COLA框架的擴展機制
至此,關于COLA 2.0的改動點我已經交代的差不多了。再追加一個彩蛋吧。泄密一下COLA作為一個框架(Framework)是如何支持擴展的。
框架作為一個組件是被集成在系統中完成某一特定任務的,比如logback作為一個日志框架是幫助我們解決打印日志、日志格式、日志存儲等問題的。但面對各種應用場景,框架本身沒辦法預測你想要的日志格式、日志歸檔的方式。這些地方需要一個擴展機制,賦能用戶自己去配置、去擴展。
就擴展的實現方式而言,一般有兩種方式,一種是基于接口的擴展,一種是基于數據配置的擴展。
基于接口的擴展
基于接口的擴展,主要是利用面向對象的多態機制,先在框架中定義一個接口(或者抽象方法)和處理該接口的模板,然后用戶實現自己的定制。 其原理如下圖所示:
這種擴展方式在框架中使用很廣泛,例如Spring中的ApplicationListener,用戶可以實現這個Listener來做容器初始化之后的特殊處理。再比如logback中的AppenderBase,用戶可以通過繼承AppenderBase實現定制的Appender訴求(往消息隊列發送日志)。
COLA作為一個框架,這樣的擴展能力在所難免,比如,我們有一個ExceptionHandlerI,在框架中我們提供了一個默認實現,代碼如下:
但是,并不是每個應用都愿意這樣的安排,因此我們提供了擴展,當用戶提供了自己ExceptionHandlerI實現的時候,優先使用用戶的實現,如果用戶沒有提供,使用默認實現:
基于數據配置的擴展
基于配置數據的擴展,首先要約定一個數據格式,然后通過利用用戶提供的數據,組裝成實例對象,用戶提供的數據是對象中的屬性(有時候也可能是類,比如slfj中的StaticLoggerBinder),其原理如下圖所示:
我們一般在應用中使用的KV配置都屬于這種形式,框架中的使用場景也很多,比如上面提到的logback中對日志格式、日志大小的logback.xml配置。
在COLA中,我們通過Annotation對擴展點的配置@Extension(bizId = "tmall", useCase = "placeOrder", scenario = "88vip"),也是一種典型的基于數據的配置擴展。
如何使用COLA 2.0
源代碼
COLA 2.0的源代碼在 https://github.com/alibaba/COLA
生成COLA應用
COLA 2.0 提供了兩套Archetype,一套是純后端應用,另一套是Web后端應用,他們的區別是Web后端應用比純后端應用多了一個Controller模塊,其它都一樣。Archetype的二方庫我已經上傳到Maven Repo了,可以通過如下命令生成COLA應用:
生成純后端應用(沒有Controller)
mvnarchetype:generate -DgroupId=com.alibaba.demo -DartifactId=demo -Dversion=1.0.0-SNAPSHOT-Dpackage=com.alibaba.demo-DarchetypeArtifactId=cola-framework-archetype-service-DarchetypeGroupId=com.alibaba.cola -DarchetypeVersion=2.1.0-SNAPSHOT
生成Web后端應用(有Controller)
mvn archetype:generate -DgroupId=com.alibaba.demo -DartifactId=demo-Dversion=1.0.0-SNAPSHOT -Dpackage=com.alibaba.demo-DarchetypeArtifactId=cola-framework-archetype-web-DarchetypeGroupId=com.alibaba.cola -DarchetypeVersion=2.1.0-SNAPSHOT
我們假設新建的應用叫demo,那么執行命令后,會看到如下的模塊結構,上部分是應用骨架,下部分是COLA框架。
在生成的應用里面有一些demo的代碼,可以直接用"mvn test"進行測試。如果是Web后端應用,可以運行TestApplication啟動Spring Boot容器,然后直接通過REST URL http://localhost:8080/customer?name=Alibaba 訪問服務。
COLA 2.0整體架構
最后,按照老規矩,還是給兩張全局的架構視圖。以便你可以從全局上把握COLA。
注意:COLA有兩層含義,一層含義是作為框架的COLA,主要提供一些應用中所需共用組件的支持。另一層含義是指COLA架構,是指通過COLA Archetype生成的應用骨架的架構。這里所說的架構視圖是應用架構視圖。
依賴視圖
調用視圖
參考資料:
【1】https://softwareengineering.stackexchange.com/questions/178927/is-there-a-difference-between-a-component-and-a-module?spm=ata.13261165.0.0.12296659zlPIXl