整潔的應用架構“長”什麼樣?

阿里妹導讀:作者張建飛是阿里巴巴高級技術專家,入司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的重要組成部分,也就是我們上面說的“指導和約束”。這些組件的詳細職責描述如下:

 

  1. 二方庫裏的組件:

    • api:存放的是應用對外的接口。

    • dto.domainmodel:用來做數據傳輸的輕量級領域對象。

    • dto.domainevent: 用來做數據傳輸的領域事件。

  2. Application裏的組件:

    • service:接口實現的facade,沒有業務邏輯,可以包含對不同終端的adapter。

    • eventhandler:處理領域事件,包括本域的和外域的。

    • executor:用來處理命令(Command)和查詢(Query),對複雜業務,可以包含Phase和Step。

    • interceptor: COLA提供的對所有請求的AOP處理機制。

  3. Domain裏的組件:

    • domain:領域實體,允許繼承domainmodel。

    • domainservice: 領域服務,用來提供更粗粒度的領域能力。

    • gateway:對外依賴的網關接口,包括存儲、RPC、Search等。

  4. 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中擴大二方庫的職責範圍。主要包括兩點:

 

  1. 二方庫中的domain model也是領域的重要組成部分,是“輕量級”的領域能力表達,所謂“輕量級”是說表達是自恰和足夠內聚的,類似於上面說的StdCategoryDO的案例。當然,能力的表達也需要遵循通用語言(Ubiquitous Language)。

  2. 不同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

文章來自:阿里技術

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章