從業這麼多年,接觸過銀行的應用,Apple的應用,eBay的應用和如今阿里的應用,雖然分屬於不一樣的公司,使用了不一樣的架構,但有一個共同點就是都很複雜。致使複雜性的緣由有不少,若是從架構的層面看,主要有兩點,一個是架構設計過於複雜,層次太多能把人繞暈。另外一個是根本就沒架構,ServiceImpl做爲上帝類包攬一切,一杆捅到DAO(就簡單場景而言,這種Transaction Script也還湊合,至少實現上手都快),這種人爲的複雜性致使系統愈來愈臃腫,愈來愈難維護,醬缸的老代碼發出一陣陣惡臭,新來的同窗,每每要捂着鼻子摳幾天甚至幾個月,才能理清系統和業務脈絡,而後又一頭扎進各類bug fix,業務修補的惡性循環中,暗無天日!
CRM做爲阿里最老的應用系統,天然也逃不過這樣的宿命。不甘如此的咱們開始反思究竟是什麼形成了系統複雜性? 咱們到底能不能經過架構來治理這種複雜性?基於這個出發點,咱們團隊開始了一段很是有意義的架構重構之旅(Redefine the Arch),期間咱們參考了SalesForce,TMF2.0,匯金和盒馬的架構,從他們那裏汲取了不少有價值的輸入,再結合咱們本身的思考最終造成了咱們本身如今的基於擴展點+元數據+CQRS+DDD的應用架構。該架構的特色是可擴展性好,很好的貫徹了OO思想,有一套完整的規範標準,並採用了CQRS和領域建模技術,在很大程度上能夠下降應用的複雜度。本文主要闡述了咱們的思考過程和架構實現,但願能對在路上的你有所幫助。html
通過咱們分析、討論,發現形成如今系統異常複雜的罪魁禍首主要來自如下四個方面:java
對於只有一個業務的簡單場景,並不須要擴展,問題也不突出,這也是爲何這個點常常被忽略的緣由,由於咱們大部分的系統都是從單一業務開始的。可是隨着支持的業務愈來愈多,代碼裏面開始出現大量的if-else邏輯,這個時候代碼開始有壞味道,沒聞到的同窗就這麼繼續往上堆,聞到的同窗會重構一下,但由於系統沒有統一的可擴展架構,重構的技法也各不相同,這種代碼的不一致性也是一種理解上的複雜度。長此以往,系統就變得複雜難維護。像咱們CRM應用,有N個業務方,每一個業務方又有N個租戶,若是都要用if-else判斷業務差別,那簡直就是慘絕人寰。其實這種擴展點(Extension Point),或者叫插件(Plug-in)的設計在架構設計中是很是廣泛的。比較成功的案例有eclipse的plug-in機制,集團的TMF2.0架構。還有一個擴展性需求就是字段擴展,這一點對SaaS應用尤其重要,由於有不少客戶定製化需求,可是咱們不少系統也沒有統一的字段擴展方案。git
是的,無論你認可與否,不少時候,咱們都是操着面向對象的語言幹着面向過程的勾當。面向對象不只是一個語言,更是一種思惟方式。在咱們追逐雲計算、深度學習、區塊鏈這些技術熱點的時候,靜下心來問問本身咱們是否是真的掌握了OOD;在咱們強調工程師要具有業務Sense,產品Sense,數據Sense,算法Sense,XXSense的時候,是否是忽略了對工程能力的要求。據我觀察大部分工程師(包括我本身)的OO能力還遠沒有達到精通的程度,這種OO思想的缺少主要體如今兩個方面,一個是不少同窗不瞭解SOLID原則,不懂設計模式,不會畫UML圖,或者只是知道,但歷來不會運用到實踐中;另外一個是不會進行領域建模,關於領域建模爭論已經不少了,個人觀點是DDD很好,但不是銀彈,用和不用取決於場景。但無論怎樣,請你拋開偏見,好好的研讀一下Eric Evans的《領域驅動設計》,若是有認知升級的感悟,恭喜你,你進階了。我我的認爲DDD最大的好處是將業務語義顯現化,把原先晦澀難懂的業務算法邏輯,經過領域對象(Domain Object),統一語言(Ubiquitous Language)將領域概念清晰的顯性化表達出來。相信我,這種表達帶來的代碼可讀性的提高,會讓接手你代碼的人對你心懷感恩的。借用Abelson的一句話是程序員
Programs must be written for people to read, and only incidentally for machines to executegithub
因此強烈譴責那些不顧他人感覺的編碼行爲。算法
俗話說的好,All problems in computer science can be solved by another level of indirection(計算機科學領域的任何問題均可以經過增長一個間接的中間層來解決),怎樣? 是否是感覺到間接層的強大了。分層最大的好處就是分離關注點,讓每一層只解決該層關注的問題,從而將複雜的問題簡化,起到分而治之的做用。咱們平時看到的MVC,pipeline,以及各類valve的模式,都是這個道理。好吧,那是否是層次越多越好,越靈活呢。固然不是,就像我開篇說的,過多的層次不只不能帶來好處,反而會增長系統的複雜性和下降系統性能。就拿ISO的網絡七層協議來講,你這個七層分的很清楚,很好,但也很繁瑣,四層就夠了嘛。再好比我前面提到的過分設計的例子,若是沒記錯的話應該是Apple的Directory Service應用,整個系統有7層之多,把什麼validator,assembler都當成一個層次來對待,能不復雜麼。因此分層太多和沒有分層都會致使系統複雜度的上升,所以咱們的原則是不能夠沒有分層,可是隻分有必要的層。編程
爲所欲爲是由於缺乏規範和約束。這個規範很是很是很是的重要(重要事情說三遍),但也是最容易被無視的點,其結果就是架構的consistency被嚴重破壞,代碼的可維護性將急劇降低,國將不國,架構將形同虛設。有同窗會說不就是個naming的問題麼,不就是個分包的問題麼,不就是2個module仍是3個module的問題麼,只要功能能跑起來,這些問題都是小問題。是的,對於這些同窗,我再丟給你一句名言「Just because you can, doesn't mean you should"。就拿package來講,它不只僅是一個放一堆類的地方,更是一種表達機制,當你將一些類放到Package中時,至關於告訴下一位看到你設計的開發人員要把這些類放在一塊兒考慮。理想很豐滿,現實很骨感,規範的執行是個大問題,最好能在架構層面進行約束,例如在咱們架構中,擴展點必須以ExtPt結尾,擴展實現必須以Ext結尾,你不這麼寫就會給你拋異常。可是架構的約束畢竟有限,更多的仍是要靠Code Review,暫時沒想到什麼更好的辦法。這種對架構約束的近似嚴苛follow,確保了系統的consistency,最終造成了一個規整的收納箱(以下圖所示),就像我和團隊說的,咱們在評估代碼改動點時,應該能夠像Hash查找同樣,直接定位到對應的module,對應的package裏面對應的class。而不是到「一鍋粥」裏去慢慢摳。
本章節最後,上一張咱們老系統中比較典型的代碼,也許你能夠從中看到你本身應用的影子。設計模式
知道了問題所在,接下來看下咱們是如何一個個解決這些問題的。回頭站在山頂再看這些解決方案時,每一個都不足爲奇,但當你還「身在此山中」的時候,這個撥開層層迷霧,看到山的全貌的過程,並非想象的那麼容易。慶幸的是我團隊在艱難跋涉以後,終有所收穫。網絡
擴展點的設計思想主要得益於TMF2.0的啓發,其實這種設計思想也一直在用,但都是在局部的代碼重構和優化,好比基於Strategy Pattern的擴展,可是一直沒有找到一個很好的固化到框架中的方法。直到毗盧到團隊分享,給了咱們兩個關鍵的提示,一個是業務身份識別,用他的話說,若是當時TMF1.0若是有身份識別的話,就沒有TMF2.0什麼事了;另外一個是抽象的擴展點機制。數據結構
業務身份識別在咱們的應用中很是重要,由於咱們的CRM系統要服務不一樣的業務方,並且每一個業務方又有多個租戶。好比中供銷售,中供拍檔,中供商家都是不一樣的業務方,而拍檔下的每一個公司,中供商家下的每一個供應商又是不一樣的租戶。因此傳統的基於多租戶(TenantId)的業務身份識別還不能知足咱們的要求,因而在此基礎上咱們又引入了業務碼(BizCode)來標識業務。因此咱們的業務身份其實是(BizCode,TenantId)二元組。在每個業務身份下面,又能夠有多個擴展點(ExtensionPoint),因此一個擴展點實現(Extension)其實是一個三維空間中的向量。借鑑Maven Coordinate的概念我給它起了個名字叫擴展座標(Extension Coordinate),這個座標能夠用(ExtensionPoint,BizCode,TenantId)來惟一標識。
擴展點的設計是這樣的,全部的擴展點(ExtensionPoint)必須經過接口申明,擴展實現(Extension)是經過Annotation的方式標註的,Extension裏面使用BizCode和TenantId兩個屬性用來標識身份,框架的Bootstrap類會在Spring啓動的時候作類掃描,進行Extension註冊,在Runtime的時候,經過TenantContext來選擇要使用的Extension。TenantContext是經過Interceptor在調用業務邏輯以前進行初始化的。整個過程以下圖所示:
面向對象不只是一種編程語言,更是一種思惟模式。因此看到不少簡歷裏面寫「精通Java」,沒寫「精通OO」,也算是中肯,由於會Java語言並不表明你就掌握了面向對象思惟(固然,精通Java也不是件易事),要想作到精通,必需要對OO設計原則,模式,方法論有很深刻的理解,同時要具有很是好的業務理解力和抽象能力,才能說是精通,這種思惟的訓練是一個長期不斷累積的過程,我也在路上,下面是我對面向對象設計的兩點體會:
SOLID是單一職責原則(SRP),開閉原則(OCP),里氏替換原則(LSP),接口隔離原則(ISP)和依賴倒置原則(DIP)的縮寫,原則是要比模式(Design Pattern)更基礎更重要的指導準則,是面向對象設計的Bible。深刻理解後,會極大的提高咱們的OOD能力和代碼質量。好比我在開篇提到的ServiceImpl上帝類的例子,很明顯就是違背了單一職責,你一個類把全部事情都作了,把不是你的功能也往本身身上攬,因此你的內聚性就會不好,內聚性差將致使代碼很難被複用,不能複用,只能複製(Repeat Yourself),其結果就是一團亂麻。
再好比在java應用中使用logger框架有不少選擇,什麼log4j,logback,common logging等,每一個logger的API和用法都稍有不一樣,有的須要用isLoggable()
來進行預判斷以便提升性能,有的則不須要。對於要切換不一樣的logger框架的情形,就更是頭疼了,有可能要改動不少地方。產生這些不便的緣由是咱們直接依賴了logger框架,應用和框架的耦合性很高。怎麼破? 遵循下依賴倒置原則就能很容易解決,依賴倒置就是你不要直接依賴我,你和我都同時依賴一個接口(因此有時候也叫面向接口的編程),這樣咱們之間就解耦了,依賴和被依賴方均可以自由改動了。
在咱們的框架設計中,這種對SOLID的遵循也是隨處可見,Service Facade設計思想來自於單一職責SRP;擴展點設計符合關閉原則OCP;日誌設計,以及Repository和Tunnel的交互就用到了依賴倒置DIP原則,這樣的點還有不少,就不一一枚舉了。固然了,SOLID不是OO的所有。抽象能力,設計模式,架構模式,UML,以及閱讀優秀框架源碼(咱們的Command設計就是參考了Activiti的Command)也都很重要。只是SOLID更基礎,更重要,因此我在這裏重點拿出來說一下,但願能獲得你們的重視。
準確的說DDD不是一個架構,而是思想和方法論。因此在架構層面咱們並無強制約束要使用DDD,但對於像咱們這樣的複雜業務場景,咱們強烈建議使用DDD代替事務腳本(TS: Transaction Script)。由於TS的貧血模式,裏面只有數據結構,徹底沒有對象(數據+行爲)的概念,這也是爲何咱們叫它是面向過程的緣由。然而DDD是面向對象的,是一種知識豐富的設計(Knowledge Rich Design),怎麼理解?,就是經過領域對象(Domain Object),領域語言(Ubiquitous Language)將核心的領域概念經過代碼的形式表達出來,從而增長代碼的可理解性。這裏的領域核心不只僅是業務裏的「名詞」,全部的業務活動和規則如同實體同樣,都須要明確的表達出來。例如前面典型代碼圖中所展現的,分配策略(DistributionPolicy)你把它隱藏在一堆業務邏輯中,沒有人知道它是幹什麼的,也不會把它當成一個重要的領域概念去重視。可是你把它抽出來,凸顯出來,給它一個合理的命名叫DistributionPolicy
,後面的人一看就明白了,哦,這是一個分配策略,這樣理解和使用起來就容易的多了,添加新的策略也更方便,不須要改原來的代碼了。因此說好的代碼不只要讓程序員能讀懂,還要能讓領域專家也能讀懂。
再好比在CRM領域中,公海和私海是很是重要領域概念,是用來作領地(Territory)劃分的,每一個銷售人員只能銷售私海(本身領地)內的客戶,不能越界。可是在咱們的代碼中卻沒有這兩個實體(Entity),也沒有相應的語言和其對應,這就致使了領域專家描述的,和咱們平常溝通的,以及咱們模型和代碼呈現的都是相互割裂的,沒有關聯性。這就給後面系統維護的同窗形成了極大的困擾,由於全部關於公海私海的操做,都是散落着各處的repeat itself的邏輯代碼,致使看不懂也沒辦法維護。因此當尚學把這兩個領域概念抽象成實體以後,整個模型和代碼都一會兒變清晰不少。在加上上面介紹的把業務規則顯現化,極大的提高了代碼的可讀性和可擴展性。用尚學的話說,用DDD寫代碼,他找到了創做的感受,而不只僅是碼農式Coding。下圖是銷售域的簡要領域模型,但基本上能表達出銷售域的核心領域概念。
關於CQRS簡要說一下,咱們只使用了Command,Query分離的概念,並無使用Event Sourcing,緣由很簡單---不須要。關於Command的實現咱們使用了命令模式,所以之前的ServiceImpl的職責就只是一個Facade,全部的處理邏輯都在CommandExecutor裏面。
這一塊的設計比較直觀,整個應用層劃分爲三個大的層次,分別是App層,Domain層和Repostiory層。
咱們規範設計主要是要知足收納原則的兩個約束:
東西不要亂放,咱們的每個組件(Module),每個包(Package)都有明確的職責定義和範圍,不能夠放錯,例如extension包就只是用來放擴展實現的,不容許放其餘東西,而Interceptor包就只是放攔截器的,validator包就只是放校驗器的。咱們的主要組件以下圖:
組件裏面的Package以下圖:
東西放在合適位置後還要貼上合適的標籤,也就是要按照規範合理命名,例如咱們架構裏面和數據有關的Object,主要有Client Object,Domain Object和Data Object,Client Object是放在二方庫中和外部交互使用的DTO,其命名必須以CO結尾,相應的Data Object主要是持久層使用的,命名必須以DO結尾。這個類名應該是自明的(self-evident),也就是看到類名就知道里面是幹了什麼事,這也就反向要求咱們的類也必須是單一職責的(Single Responsibility)的,若是你作的事情不單純,天然也就很難自明瞭。若是咱們Class Name是自明的,Package Name是自明的,Module Name也是自明的,那麼咱們整個應用系統就會很容易被理解,看起來就會很舒服,維護效率會提升不少。咱們的命名規則以下圖所示:
通過上面的長篇大論,我但願我把咱們的架構理念闡述清楚了,最後再從總體上看下咱們的架構吧。我講這個架構命名爲SOFA,全稱是Simple Object-oriented and Flexible Architecture,是一個輕量級的面向對象的,可擴展的應用架構,能夠幫助下降複雜應用場景的系統熵值,提高系統開發和運維效率。
目前框架也準備開源,貢獻個社區,讓更多的開發者使用,幫助解決他們各自的業務複雜度。
關於框架源碼和介紹,請移步:https://github.com/alibaba/SOFA/
咱們的架構原則很簡單,即在高內聚,低耦合,可擴展,易理解大的指導思想下,儘量的貫徹OO的設計思想和原則。咱們最終造成的架構是集成了擴展點+元數據+CQRS+DDD的思想,關於元數據前面沒怎麼提到,這裏稍微說一下,對於字段擴展,簡單一點的解決方案就是預留擴展字段,複雜一點的就是使用元數據引擎。使用元數據的好處是不只能支持字段擴展,還提供了豐富的字段描述,等因而爲之後的SaaS化配置提供了可能性,因此咱們選擇了使用元數據引擎。和DDD同樣,元數據也是可選的,若是對沒有字段擴展的需求,就不要用。最後的總體架構圖以下:
由於框架包含了5個Module,20+的Package,若是手動建立的話很費時,並且很容易出錯,因此建立了這個Archetype,能夠一鍵構建框架的全部Artifacts,使用時,只須要將下面的命令中的demo替換成本身應用的名字便可:
mvn archetype:generate -DgroupId=com.alibaba.crm -DartifactId=demo -Dversion=1.0.0-SNAPSHOT -Dpackage=com.alibaba.crm.demo -DarchetypeArtifactId=sofa-framework-archetype -DarchetypeGroupId=com.alibaba.sofa -DarchetypeVersion=1.0.0-SNAPSHOT
無論你是否是TDD吧,寫幾行代碼,而後本地跑下測試驗證一下老是個不錯的習慣。由於代碼仍是熱的,出錯也容易定位。可是本地啓動PandoraBoot可不是個省心的事,我這臺2.3G雙核平均也要4分鐘,嚴重的影響了效率。因此開發了這個工具,就是等PandoraBoot啓動後,將線程Hold住,而後經過Console控制檯輸入要測試的方法或者類。使用這個工具很簡單。
首先依賴crm-test:
<dependency> <groupId>com.alibaba.crm</groupId> <artifactId>crm-test</artifactId> <version>1.0.0-SNAPSHOT</version> <scope>test</scope> </dependency>
而後將TestApplication修改以下便可:
public class TestApplication { public static void main(String[] args) { PandoraBootstrap.run(args); SpringApplication.run(Application.class, args); TestsContainer.start();//啓動測試容器,避免重複啓動PandoraBoot } }
轉載自:http://blog.csdn.net/significantfrank/article/details/79286947