阿里高級技術(shù)專家:整潔的應(yīng)用架構(gòu)“長”什么樣?
作者張建飛是阿里巴巴高級技術(shù)專家,入司6年,他創(chuàng)建了COLA。希望可以探索一套切實可行的應(yīng)用架構(gòu)規(guī)范,這個規(guī)范不是高高在上的紙上談兵,而是可以復(fù)制、可以理解、可以落地、可以控制復(fù)雜性的指導(dǎo)和約束。本文詳述了他對COLA的升級迭代。
很多同學(xué)不止一次和我反饋,我們的系統(tǒng)很混亂,主要表現(xiàn)在:
- 應(yīng)用的層次結(jié)構(gòu)混亂:不知道應(yīng)用應(yīng)該如何分層、應(yīng)該包含哪些組件、組件之間的關(guān)系是什么;
- 缺少規(guī)范的指導(dǎo)和約束:新加一段業(yè)務(wù)邏輯不知道放在什么地方(哪個類,哪個包)、應(yīng)該起什么名字比較合適?
解決這些問題,正是我創(chuàng)建COLA(https://github.com/alibaba/COLA)的初心之一——試圖探索一套切實可行的應(yīng)用架構(gòu)規(guī)范,這個規(guī)范不是高高在上的紙上談兵,而是可以復(fù)制、可以理解、可以落地、可以控制復(fù)雜性的指導(dǎo)和約束。
自從COLA誕生以來,我收到了很多的意見和建議。同時,我自己在實踐過程中,也發(fā)現(xiàn)COLA 1.0的諸多不足,有些設(shè)計是冗余的并不是很有必要,而有些關(guān)鍵要素并沒有囊括。譬如,我最近在思考的應(yīng)用架構(gòu)核心和復(fù)雜業(yè)務(wù)代碼治理就是對COLA 1.0的反思。
結(jié)合實踐中的探索和對復(fù)雜度治理持續(xù)的思考,我決定對COLA進行一次全面的升級,于是有了現(xiàn)在的COLA 2.0。
從1.0到2.0,不僅僅是數(shù)字的簡單變化,更是架構(gòu)理念和設(shè)計理念的升級,其主要變動點包括:
- 新架構(gòu)分層:Domain層不再直接依賴Infrastructure層。
- 新組件劃分:對組件進行了重新定義和劃分,加了新組件,去除了一些老組件(Validator,Convertor等)。
- 新擴展點設(shè)計:引入了新概念,讓擴展更加靈活。
- 新二方庫定位:二方庫不僅僅是DTO,也是Domain Model的輕量級表達和實現(xiàn)。
新架構(gòu)分層
在COLA 1.0中,我們的分層是如下圖所示的經(jīng)典分層結(jié)構(gòu):
在COLA 2.0中,還是這些層次,但是依賴關(guān)系發(fā)生了變化,Domain層不再直接依賴Infrastructure層,而是引入了一個Gateway的概念,使用DIP(Dependency Inversion Principle,依賴倒置)反轉(zhuǎn)了Domain層和Infrastructure層的依賴關(guān)系,其關(guān)系如下圖所示:
這樣做的好處是Domain層會變得更加純粹,完全擺脫了對技術(shù)細節(jié)(以及技術(shù)細節(jié)帶來的復(fù)雜度)的依賴,只需要安心處理業(yè)務(wù)邏輯就好了。
除此之外,還有兩個好處:
1. 并行開發(fā):只要在Domain和Infrastructure之間約定好接口,可以有兩個同學(xué)并行編寫Domain和Infrastructure的代碼。2. 可測試性:沒有任何依賴的Domain里面都是POJO的類,單元測試將會變得非常方便,也非常適合TDD的開發(fā)。
新組件劃分
模塊和組件的定義
首先,先明確一下組件(Component)這個概念的定義,組件在Java中(或者說在本文中),其范圍就是Java的包(Package)。
還有一個詞叫模塊(Module),組件和模塊這兩個概念是比較容易發(fā)生混淆的。比如在《實現(xiàn)領(lǐng)域驅(qū)動設(shè)計》中,作者就說:
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之間區(qū)別的。獲得最高贊的答案是通過Scope來區(qū)分的。
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.
這個回答和我的直覺反應(yīng)是一致的,即Module比Component要大。根據(jù)以上信息,我在此對Module和Component進行一下定義說明,在本文中,都會遵照如下的定義和Notation(表示法)。
- 模塊(Module):和Maven中Module定義保持一致,簡單理解就是Jar。用正方體表示。
- 組件(Component):和UML中的定義類似,簡單理解就是Package。用UML的組件圖表示。
一個Moudle通常是由多個Component組成的,其關(guān)系和表示法如下圖所示:
COLA 2.0的組件
在COLA 2.0中,我們重新設(shè)計了組件,引入了一些新的組件,也去除了一些舊組件。這些變動的宗旨是為了讓應(yīng)用結(jié)構(gòu)更加清晰,組件的職責更加明確,從而更好的提供開發(fā)指導(dǎo)和約束。
新的組件結(jié)構(gòu)如下圖所示:
這些組件各自都有自己的職責范圍,組件的職責是COLA的重要組成部分,也就是我們上面說的“指導(dǎo)和約束”。這些組件的詳細職責描述如下:
二方庫里的組件:
- api:存放的是應(yīng)用對外的接口。
- dto.domainmodel:用來做數(shù)據(jù)傳輸?shù)妮p量級領(lǐng)域?qū)ο蟆?/li>
- dto.domainevent: 用來做數(shù)據(jù)傳輸?shù)念I(lǐng)域事件。
Application里的組件:
- service:接口實現(xiàn)的facade,沒有業(yè)務(wù)邏輯,可以包含對不同終端的adapter。
- eventhandler:處理領(lǐng)域事件,包括本域的和外域的。
- executor:用來處理命令(Command)和查詢(Query),對復(fù)雜業(yè)務(wù),可以包含Phase和Step。
- interceptor: COLA提供的對所有請求的AOP處理機制。
Domain里的組件:
- domain:領(lǐng)域?qū)嶓w,允許繼承domainmodel。
- domainservice: 領(lǐng)域服務(wù),用來提供更粗粒度的領(lǐng)域能力。
- gateway:對外依賴的網(wǎng)關(guān)接口,包括存儲、RPC、Search等。
Infrastructure里的組件:
- config:配置信息相關(guān)。
- message:消息處理相關(guān)。
- repository:存儲相關(guān),是gateway的特化,主要用來做本域的數(shù)據(jù)CRUD操作。
- gateway:對外依賴的網(wǎng)關(guān)接口(Domain里的gateway)的實現(xiàn)。
在使用COLA的時候,請盡量按照組件規(guī)范約束去構(gòu)建我們的應(yīng)用。這樣可以讓我們的應(yīng)用結(jié)構(gòu)清晰、有章可循。如此這般,代碼的可維護性和可理解性會得到極大的提升。
新擴展點設(shè)計
引入新概念
在討論之前,我們先來明確一下在COLA2.0擴展設(shè)計中引入的新概念:業(yè)務(wù)、用例、場景。
- 業(yè)務(wù)(Business):就是一個自負盈虧的財務(wù)主體,比如tmall、淘寶和零售通就是三個不同的業(yè)務(wù)。
- 用例(Use Case):描述了用戶和系統(tǒng)之間的互動,每個用例提供了一個或多個場景。比如,支付訂單就是一個典型的用例。
- 場景(Scenario):場景也被稱為用例的實例(Instance),包括用例所有的可能情況(正常的和異常的)。比如對于“訂單支付”這個用例,就有“可以使用花唄”,“支付寶余額不足”,“銀行賬戶余額不足”等多個場景。
簡單來說,就是一個業(yè)務(wù)是有多個用例組成的,一個用例是有多個場景組成的。用淘寶做一個簡單示例,業(yè)務(wù)、用例和場景的關(guān)系如下:
新擴展點的實現(xiàn)
在COLA 2.0中,擴展的實現(xiàn)機制沒有變化,主要變化就在于上文中引入的新概念。因為COLA 1.0的擴展設(shè)計思想來自于星環(huán),所以當初的擴展粒度也是copy了星環(huán)的“業(yè)務(wù)身份”。COLA 1.0的擴展定位的方法如下圖所示:
然而,在實際工作中,能像星環(huán)那樣支撐多個業(yè)務(wù)的場景并不常見。更多是對不用用例,或是對同一個用例不同場景的差異化支持。比如“創(chuàng)建商品”和“更新商品”是兩個用例,但是大部分的業(yè)務(wù)代碼是可以復(fù)用的,只有一小部分需要差異化處理。
為了支持這種更細粒度的擴展支持,除了之前的“業(yè)務(wù)身份(BizId)”之外,我還引入了Use Case和Scenario這兩個概念。新的擴展定位如下圖所示:
可以看到,在新的擴展框架下,原來只能支持到“業(yè)務(wù)身份”的擴展,現(xiàn)在可以支持到“業(yè)務(wù)身份”,“用例”,“場景”的三級擴展,無疑比以前要靈活的多,并且在表達和可理解性上也比以前好。
在新的擴展框架下,例如我們實現(xiàn)上圖中所展示的擴展:在tmall這個業(yè)務(wù)下——的下單用例——的88VIP場景——的用戶身份校驗進行擴展,我們只需要聲明一個如下的擴展實現(xiàn)(Extension)就可以了。
新二方庫定位
關(guān)于二方庫的定位表面上來看,是一個簡單問題,因為服務(wù)的二方庫無外乎就是用來暴露接口和傳遞數(shù)據(jù)的(DTO)。不過,往深層次思考,它并不是一個簡單的問題,因為它涉及到不同界限上下文(Bounded Context)之間的協(xié)作問題。 它是分布式環(huán)境下,不同服務(wù)(SOA,RPC,微服務(wù),叫法不同,本質(zhì)一樣)之間如何協(xié)作的重要架構(gòu)設(shè)計問題。
Bounded Context之間的協(xié)作
如何實現(xiàn)不同域之間的協(xié)作,同時又要保證各自領(lǐng)域的概念的完整性是有一套方法論的??傮w來說,大概有兩種方式:共享內(nèi)核(Shared Kernel)和防腐層(ACL,Anti-Corruption Layer)。
1. 共享內(nèi)核(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)關(guān)于Shared Kernel描述的原話,其優(yōu)點是Share(減少重復(fù)建設(shè)),其缺點也是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》, 防腐層是隔離最徹底的做法,其優(yōu)點是沒有Share(完全解耦,各自獨立),其缺點也是沒有Share(有一定的轉(zhuǎn)換成本)。
不過我和Vernon的觀點差不多,都比較贊成防腐層的做法。因為增加的語義轉(zhuǎn)換陳本,相較于系統(tǒng)的可維護性和可理解性而言,是完全值得的。
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.
二方庫的重新定位
在大部分情況下,二方庫的確是用來定義服務(wù)接口和數(shù)據(jù)協(xié)議的。但是二方庫區(qū)別于JSON的地方是它不僅僅是協(xié)議,它還是一個Java對象,一個Jar包。
既然是Java對象,就意味著我們就有可能讓DTO承載除了getter,setter之外的更多職能。這個問題以前沒有引起我的重視,但是最近在思考domain model的時候,我發(fā)現(xiàn),我們是可以在讓二方庫承擔更多職責的,發(fā)揮更大的作用。
實際上,在阿里,我發(fā)現(xiàn)有些團隊已經(jīng)在這么實踐了,而且我覺得效果還不錯。比如,中臺的類目二方庫,在這個事情上就做了比較好的示范。類目是商品中比較復(fù)雜的邏輯,里面涉及很多計算,我們先看一下類目二方庫的代碼是怎么寫的:
從上面的代碼,我們可以發(fā)現(xiàn)這已經(jīng)遠遠超出DTO的范疇了,這就是一個Domain Model(有數(shù)據(jù),有行為,有繼承)。這樣做合適嗎?我認為是合適的:
- 首先,DefaultStdCategoryDO用到的所有數(shù)據(jù)都是自恰的,即這些計算是不需要借助外面的輔助,自己就能完成。比如判斷是否是根類目,是否是葉子類目,獲取類目的名稱路徑等,都是依靠自己就能完成。
- 其次,這就是一種共享內(nèi)核,我把自己領(lǐng)域的知識(語言、數(shù)據(jù)和行為)通過二方庫暴露出去了,假如有100個應(yīng)用需要使用isRoot( )做判斷,你們都不需要自己實現(xiàn)了。
什么?不是說不推薦共享內(nèi)核的做法嗎?(好吧,小孩子才分對錯,好嗎)。此處的共享內(nèi)核我認為是有積極意義的,特別是類目這種輕數(shù)據(jù)、重計算的場景。不過,共享帶來的緊耦合也的確是一個問題。所以如果我是類目服務(wù)的Consumer的話,我會選擇用一個Wrapper去對Category進行包裝復(fù)用,這樣既可以復(fù)用它的領(lǐng)域能力,又可以起到隔離防腐的作用。
COLA中的二方庫
說到這里,我想你應(yīng)該已經(jīng)理解我對二方庫的態(tài)度了。是的,二方庫不應(yīng)該僅僅是接口和DTO,而是領(lǐng)域的重要組成部分,是實現(xiàn)Shared Kernel的重要手段。
因此,我打算在COLA 2.0中擴大二方庫的職責范圍。主要包括兩點:
二方庫中的domain model也是領(lǐng)域的重要組成部分,是“輕量級”的領(lǐng)域能力表達,所謂“輕量級”是說表達是自恰和足夠內(nèi)聚的,類似于上面說的StdCategoryDO的案例。當然,能力的表達也需要遵循通用語言(Ubiquitous Language)。
不同Bounded Context之間的協(xié)作,要充分利用好二方庫的橋梁作用。其協(xié)作方式如下圖所示。
注意,這只是建議,不是標準。實際上,我們永遠要在共享和耦合之間做一個權(quán)衡,世界上沒有完美的架構(gòu),也沒有完美的設(shè)計。 合不合適,還需要你自己根據(jù)實際場景自己去定奪。
COLA框架的擴展機制
至此,關(guān)于COLA 2.0的改動點我已經(jīng)交代的差不多了。再追加一個彩蛋吧。泄密一下COLA作為一個框架(Framework)是如何支持擴展的。
框架作為一個組件是被集成在系統(tǒng)中完成某一特定任務(wù)的,比如logback作為一個日志框架是幫助我們解決打印日志、日志格式、日志存儲等問題的。但面對各種應(yīng)用場景,框架本身沒辦法預(yù)測你想要的日志格式、日志歸檔的方式。這些地方需要一個擴展機制,賦能用戶自己去配置、去擴展。
就擴展的實現(xiàn)方式而言,一般有兩種方式,一種是基于接口的擴展,一種是基于數(shù)據(jù)配置的擴展。
基于接口的擴展
基于接口的擴展,主要是利用面向?qū)ο蟮亩鄳B(tài)機制,先在框架中定義一個接口(或者抽象方法)和處理該接口的模板,然后用戶實現(xiàn)自己的定制。 其原理如下圖所示:
這種擴展方式在框架中使用很廣泛,例如Spring中的ApplicationListener,用戶可以實現(xiàn)這個Listener來做容器初始化之后的特殊處理。再比如logback中的AppenderBase,用戶可以通過繼承AppenderBase實現(xiàn)定制的Appender訴求(往消息隊列發(fā)送日志)。
COLA作為一個框架,這樣的擴展能力在所難免,比如,我們有一個ExceptionHandlerI,在框架中我們提供了一個默認實現(xiàn),代碼如下:
但是,并不是每個應(yīng)用都愿意這樣的安排,因此我們提供了擴展,當用戶提供了自己ExceptionHandlerI實現(xiàn)的時候,優(yōu)先使用用戶的實現(xiàn),如果用戶沒有提供,使用默認實現(xiàn):
基于數(shù)據(jù)配置的擴展
基于配置數(shù)據(jù)的擴展,首先要約定一個數(shù)據(jù)格式,然后通過利用用戶提供的數(shù)據(jù),組裝成實例對象,用戶提供的數(shù)據(jù)是對象中的屬性(有時候也可能是類,比如slfj中的StaticLoggerBinder),其原理如下圖所示:
我們一般在應(yīng)用中使用的KV配置都屬于這種形式,框架中的使用場景也很多,比如上面提到的logback中對日志格式、日志大小的logback.xml配置。
在COLA中,我們通過Annotation對擴展點的配置@Extension(bizId = "tmall", useCase = "placeOrder", scenario = "88vip"),也是一種典型的基于數(shù)據(jù)的配置擴展。
如何使用COLA 2.0
源代碼
COLA 2.0的源代碼在 https://github.com/alibaba/COLA
生成COLA應(yīng)用
COLA 2.0 提供了兩套Archetype,一套是純后端應(yīng)用,另一套是Web后端應(yīng)用,他們的區(qū)別是Web后端應(yīng)用比純后端應(yīng)用多了一個Controller模塊,其它都一樣。Archetype的二方庫我已經(jīng)上傳到Maven Repo了,可以通過如下命令生成COLA應(yīng)用:
生成純后端應(yīng)用(沒有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后端應(yīng)用(有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
我們假設(shè)新建的應(yīng)用叫demo,那么執(zhí)行命令后,會看到如下的模塊結(jié)構(gòu),上部分是應(yīng)用骨架,下部分是COLA框架。
在生成的應(yīng)用里面有一些demo的代碼,可以直接用"mvn test"進行測試。如果是Web后端應(yīng)用,可以運行TestApplication啟動Spring Boot容器,然后直接通過REST URL http://localhost:8080/customer?name=Alibaba 訪問服務(wù)。
COLA 2.0整體架構(gòu)
最后,按照老規(guī)矩,還是給兩張全局的架構(gòu)視圖。以便你可以從全局上把握COLA。
注意:COLA有兩層含義,一層含義是作為框架的COLA,主要提供一些應(yīng)用中所需共用組件的支持。另一層含義是指COLA架構(gòu),是指通過COLA Archetype生成的應(yīng)用骨架的架構(gòu)。這里所說的架構(gòu)視圖是應(yīng)用架構(gòu)視圖。
依賴視圖
調(diào)用視圖
參考資料:
【1】https://softwareengineering.stackexchange.com/questions/178927/is-there-a-difference-between-a-component-and-a-module?spm=ata.13261165.0.0.12296659zlPIXl













































