事件驅動的微服務-整體設計

事件驅動的微服務-整體設計git

我在"微服務之間的最佳調用方式"中講到了微服務之間的兩種調用方式。微服務剛興起時,大部分都是RPC的調用模式。我也寫了一個RPC的架構,詳情參見"清晰架構(Clean Architecture)的Go微服務"。但如今事件驅動的微服務愈來愈流行,由於你們以爲它是鬆耦合的。我會寫一個新的系列來說述如何構建事件驅動的微服務。本文是這個系列的第一篇,整體設計。github

本文經過一個具體的例子來說解事件驅動微服務的設計,它包含兩個微服務,一個是訂單服務(Order),另外一個是支付服務(Payment),它們各自獨立,每一個服務有本身的源碼庫,數據庫,各自單獨部署。它們之間經過事件驅動的方式互相調傭。數據庫

在拿到一個新的項目時,咱們有時會以爲不知道從哪下手。最一般的的思路是化繁爲簡,把一個大的項目分解爲一個個小的部分再各個擊破。把程序分層也是這個思路的具體應用。這裏咱們仍然沿用RPC時的架構,按照清晰架構把業務邏輯分紅三層,域模型(model),用例(usecase)和數據服務(dataservice)。在事件驅動模式時會增長新的層,就是事件層(Event),它包含事件和事件驅動器(Event Handler)。在本文中,你將會讀到如何對原來的架構進行擴展,增長事件處理功能。後端

本文從下面三個方面講解程序的設計:架構

  • 模塊設計
  • 框架(Framework)和庫(Library)
  • 共享程序和第三方庫

它們是設計中最基本的也是最重要的東西,有了它其餘的東西才能在這個基礎之上創建起來。app

模塊設計:

程序設計中的很重要的一步就是把程序的功能拆分紅小的模塊,並把程序分層,而後把這些模塊放入相應的層級,最後確立模塊之間的依賴關係。框架

程序分層

本程序基本延用了清晰架構的原則,但因爲增長了事件驅動的部分,所以我引入了"Domain-Driven Design"的一些概念對清晰架構進行了一些改造。畢竟事件驅動的不少概念都是由DDD(Domain-Driven Design)提出來的,它有關於事件驅動的一整套理論和實踐。對於DDD的理解,不一樣的人的解釋可能稍有差異,其中最著名的應該來自於Eric Evans的書"Domain-Driven Design: Tackling Complexity in the Heart of Software"。但由於它稍微有一點虛,有些概念並無明確的代碼,所以可能會有不一樣的解讀。因爲這個緣由,我採用了"Patterns, Principles, and Practices of Domain-Driven Design"這本書中對DDD的解釋,它裏面全部的概念都有具體的代碼和實例,所有都是落了地的東西,這樣至少不會產生歧義。dom

在「Pattern,Principles,and Practices of Domain-Driven Design」這本書中,它列舉了DDD的8個組成模塊,它們是值對象(Value Object),實體(Entities),域服務(Domain Service),域事件(Domain Event),聚合根(Aggregates),工廠(Factories),倉儲(Respository)和事件溯源(Even Sourcing)。其中值對象,實體和聚合根都是域模型。域服務就是清晰架構中的用例。工廠(Factories)是用來建立類的,也就至關於Spring裏的程序容器。倉儲(Respository)就是數據持久層。模塊化

咱們來看一下怎樣把它們映射到清晰架構。首先,事件溯源(Even Sourcing)不是DDD的組成模塊,而是一種實現方式(你能夠用它,也能夠不用),咱們先把它去掉。剩下的7個模塊是咱們須要的。對於如何將DDD分層,你們的意見也並不統一,不過大體能夠分紅四層,領域層(Domain Layer),程序層(Application Layer),基礎設施層(Infrastructure Layer)和用戶界面層(User Interface Layer)。本文的重點在後端程序,因此咱們只討論前三層(把用戶界面層去掉)。對於前三層的解釋和應該包含的模塊,你們也有不一樣意見。我先來說一下上面書中的解釋。對領域層(Domain Layer)的分歧較少,它主要是處理領域的業務邏輯。程序層(Application Layer)主要有三個功能,第一是用例,也就是有些業務邏輯涉及到多個域模型,放到那個單個模型都不合適,就放到程序層;第二是商業過程(Business Process),就是有些業務邏輯有流程,也須要涉及到多個域模型。第三是業務邏輯要調用外部的一些功能,例如發郵件,發消息。這部分又分紅兩塊,一塊是接口定義,放在程序層中,另外一部分是具體實現,放在基礎設施層(Infrastructure Layer)。函數

整體來講,這個分法仍是比較靠譜的,可是我對它的一些細節仍是有不一樣見解的。首先,用例裏主要仍是業務邏輯,只不過是跨了多個域模型,確定仍是應該放在領域層。其次,商業過程(Business Process)業務主要仍是業務邏輯,也應該放在領域層。程序層(Application Layer)應該只有對外服務的接口,這樣也符合書中對程序層的描述。由於做者一直在講,程序層要儘可能小。領域層纔是大頭。

若是咱們把上面提到的7個模塊分到不一樣的層中,我·以爲應該是這樣的:

  • 領域層(Domain Layer): 包括聚合根(Aggregates),值對象(Value Object),實體(Entities),域服務(Domain Service),倉儲(Respository),域事件(Domain Event)
  • 程序層(Application Layer): 包括外部服務的接口
  • 基礎設施層(Infrastructure Layer): 包括外部服務接口的具體實現

工廠(Factories)不在上述任何一層裏面,它其實就是程序容器,能夠單獨列爲一層。

有一點須要說明的是我並無徹底採用DDD的架構,本程序的主要框架仍是清晰架構,但因爲清晰架構裏沒有對事件驅動的明確指引,所以我引入了DDD的事件驅動部分來對清晰架構進行改造和擴充,但總的來說,本設計的底子仍是清晰架構。

程序結構:

下面咱們就講一下在程序中是如何實現上面講到的模塊化和分層結構。

folderStructure.png

上面就是訂單服務的目錄結構,其中「Domain」對應領域層, 「applicationservice」對應程序層和基礎設施層,「app」對應程序容器。

領域層:

domain.png

上面就是領域層的目錄結構,這層是整個程序的大頭。這層包含有命令(Command),事件(Event),域模型(Model)和用例(Usecase)。其中命令(Command)和事件(Event)是事件驅動模式獨有的。Event目錄裏有事件(Event)和事件驅動器(Event Handler)。

事件會在兩個或多個微服務之間傳遞,所以是被這些微服務共享的。一個問題是,要不把這些事件抽出來放在一個單獨的模塊中,這樣不一樣的微服務就能夠共享這些事件?這不是一個好主意,由於它會增大微服務之間的耦合度。儘管事件是被多個微服務共享的,但實際上它們在各個微服務裏可能並不徹底相同。例如,支付微服務發送一個支付完成事件給訂單微服務,支付微服務須要在事件中增長一個字段「支付備註」,而訂單微服務並不想立刻就用這個字段,若是共享事件的話就比較麻煩。若是支付微服務和訂單微服務各自單獨立地定義事件,就保持了各自的獨立性。雖然傳遞的事件裏有「支付備註」字段,但訂單服務能夠選擇忽略它(訂單服務沒必要修改代碼)。這樣雖然有一些重複代碼,但維護起來更方便。

域模型設計與RPC的域模型基本相同,我這裏不仔細講,有興趣的請參見清晰架構(Clean Architecture)的Go微服務: 程序設計。稍有不一樣的是在事件驅動模式下,引入了「eventbus」(是一個接口)的概念用來處理事件,在業務邏輯裏須要調用這個接口,所以須要把「eventbus」注入到用例(usecase)裏。

程序層:

applicationservice.png

上面就是程序層的目錄結構。這層如今只有一個服務,數據庫服務,。其實還有另外三個服務,一個是日誌服務,一個是消息服務,另外一個是事件總線服務(Eventbus),但因爲這三個服務都是在第三方庫裏定義的,所以就沒有放在訂單服務的程序層裏。
其實程序層裏的大部分接口都是能夠共享的,那麼是否是應該把他們都定義成共享庫呢?我以爲是能夠的,但若是隻有接口定義(沒有具體實現)的話,它應該很小,放在項目裏也沒有太大的問題。

基礎設施層:

如今,這一層只有數據庫服務的具體實現。日誌,消息和事件總線服務(Eventbus)的實現都在第三方庫中。程序層和基礎設施層雖然屬於不一樣層,但在如今的目錄結構中是放在一塊兒的,並無把他們分開,你若是要把它們分開也沒有問題。

程序容器:

這個也是單獨的一部分,由於比較複雜,我會在本系列的一篇文章「事件驅動的微服務-程序容器設計」裏單獨講解。

倉儲(Respository)

一個比較有爭議的地方應該是倉儲(Respository),在DDD中是把他放在領域層。其實不單是DDD,幾乎任何框架都把它放在領域層。我在寫RPC的微服務時也是把它放在領域層。但若是你仔細想的話,倉儲(Respository)是數據庫的具體實現,按照DDD的理論是應該放在基礎設施層。但因爲幾乎全部的框架都是把它放在領域層,咱們已經養成了習慣,天然而然這麼作了,根本沒有仔細考慮。

另一個緣由就是倉儲中的數據對域模型確實比較重要,所以把它放在離域模型近的地方可能比較方便。但我這裏仍是按照規則把它放在程序層,若是之後以爲有問題再改也不遲。

依賴關係:

在程序設計中,先要把程序拆成小的相對獨立的模塊,而後就是要肯定各部分之間的依賴關係。這是程序設計裏很是重要的一步。

依賴關係都是單向的,若是出現了循環依賴,Go就會報錯。依賴關係都是從上往下的,也就是上層依賴下層。越是下層的東西越容易複用,由於它依賴的東西少。越是上層的東西越重,由於有太多的依賴關係。所以衡量程序的好壞,一個重要的指標就是它所依賴的的庫,依賴的越少,程序的質量越高。在Go語言裏,就是看「import」語句。「import」越少,程序越好。

依賴關係乍一看很簡單,但仔細研究的話仍是有很多內容的。它分類爲,層級依賴關係,包依賴關係,接口依賴和實現依賴。下面會仔細講解。

層級依賴關係:

咱們先來看大的層級。領域層和基礎設施層之間的關係,確定是領域層依賴基礎設施層。但若是是直接依賴具體實現,那麼就把領域層和具體的基礎設施實現綁定了,所以須要解耦。就建立了一個程序層,這樣領域層和基礎設施層都依賴程序層,就解除了綁定。

在領域層內部,它裏面又有小的層次,命令,事件,域模型和用例。其中域模型不須要依賴任何一層,而別人都須要依賴他,所以他是最底層。命令和事件都有可能調用用例,所以它們是用例的上層。而命令和事件應該是互相獨立的,所以沒有依賴關係。

依賴關係性質

依賴關係有兩種,一種是接口依賴,另外一種是具體實現依賴。好比容器層(「app」)和領域層(「domain」)之間的關係就是"app"依賴"domain"(主要是依賴「domain」裏的「model」),而"domain"不依賴「app」。你可能要問,爲何會是這樣呢?"domain"裏要用到「app」創建的類呀。注意"domain"裏用到的是接口,而不是具體的類,接口是在"domain"裏定義的,而不是在「app」裏定義的。所以,接口依賴是一種很是靈活的依賴關係,是鬆耦合的。

包依賴關係:

層級依賴是抽象的依賴關係,但最終仍是要落實到語言層面。在Go語言中就是包依賴關係,這是Go語言的最細顆粒度的依賴關係。在Go語言中不能產生循環依賴,不然報錯。

包依賴關係和層級依賴關係大部分時候是一致的,但有時因爲種種願因,它們也會出現錯位。例如,數據持久邏輯是放在基礎設施層裏的,它是不該該依賴域模型的。但在本程序中倒是。實際上,數據持久層裏的域模型應該被替換成DTO,而DTO不是屬於域模型,這樣就不會出現錯位依賴。但若是引入DTO會讓程序更復雜,又沒有增長新的功能,所以就沒有引入。實際上,DTO和域模型都是面向對象的概念,你若是用面向函數的概念來思考就順暢了。在面向函數的模式裏,就只有數據(Data)和函數(function),所以DTO和域模型都是數據,其實是一個東西。

結構修改:

我在本程序裏對原來的程序結構(參見清晰架構(Clean Architecture)的Go微服務: 程序結構)作了一點小的修改。原來的結構並無「domain」這個目錄,如今我增長了這個目錄,並把全部與與業務邏輯相關的目錄都放在它之下,這樣程序結構更合理。其實,我在寫上個框架(RPC)時就有了這個想法,但當時框架已經寫完了,就沒有再改。如今增長了事件驅動的功能,就更凸顯了更改的必要性。以訂單服務爲例,它的主要功能都包含在兩個目錄裏,「app」是程序容器,「domain」(領域)是業務邏輯。「domain」裏面有四個目錄,「model」是域模型,「usecase」是用例,「event」包含事件和事件驅動器(Event Handler),「command」是命令。其中「event」和「command」是事件驅動模式獨有的,其它的與RPC模式是同樣的。

框架(Framework)和庫(lib)

當完成了程序的層級劃分和模塊拆分以後,下一步就是決定程序的框架。我在寫RPC的時候沒有使用現成的框架,而是本身寫了一個。在作事件驅動的微服務時,我考慮了很長時間是否是要嘗試一下使用現成的框架,這樣就有機會比較外面的框架和本身的框架的區別。Go語言有很多很好的微服務框架例如Go kitGo Micro,它們的功能都很強大。但我最終沒有選擇他們主要是它們都包含了太多我不須要的東西,有些重,所以我仍是決定在使用本身原來的框架。如今程序已經完成,我對結果仍是很滿意的。

共享代碼和第三方庫

一個程序會用到許多模塊,有些須要你本身編寫,另一些能夠直接使用第三方的現成庫。例如本程序的事件驅動部分和SQL驅動程序都使用的是第三方庫。值得慶幸的是Go語言的基本庫很是強大,已經能完成許多功能,一般狀況下不須要太多的第三方庫。另外就是你本身的不一樣程序之間會共享一些功能,若是把它們放在各自的程序裏就會有重複代碼。在寫本程序時,我把一些共享功能從程序裏抽出來,寫成了第三方庫。例如日誌功能和消息中間件接口。這樣作的一個好處就是這些庫是不依賴於框架的,任何程序均可以用它。我會在本系列的一篇文章「事件驅動的微服務-建立第三方庫」裏詳細講解。

總結:

碼農都有本身獨特的方式來判斷程序的好壞。有的嗅覺靈敏,用鼻子來聞程序是否是有「Code Smell」。有的用眼睛來看。
通過這樣的設計以後,整個程序的結構已經很順暢了,它看起來就像一件藝術品。所以人們說程序設計是科學和藝術的結合。若是說有什麼瑕疵的話,那就是前面講到的基礎設施層中的數據庫代碼裏有對域模型的依賴,這種依賴關係是不該該出現的。若是要讓它完美就要把域模型改爲DTO(Data Transfer Object),但這樣改過以後會讓程序更復雜,又沒有增長新的功能,只是從設計角度看更漂亮了。我畢竟是碼農,最重要的是用最簡單的方法來完成須要的功能。所以,只好忍痛容忍這點瑕疵了。

最重要的是把程序的結構理順了以後,整個程序是用一個一個小的模塊搭建起來的,各個模塊之間之間的依賴關係簡潔明確,大大地簡化了之後程序升級,複用和維護的難度。就像蓋一棟大樓,若是地基牢固,整個結構設計合理,裏面再怎麼裝修,折騰也不會倒塌。

源程序:

完整的源程序連接:

索引:

1 微服務之間的最佳調用方式

2 清晰架構(Clean Architecture)的Go微服務

3 Domain-Driven Design

4 Domain-Driven Design: Tackling Complexity in the Heart of Software

5 Patterns, Principles, and Practices of Domain-Driven Design

6 清晰架構(Clean Architecture)的Go微服務: 程序設計

7 清晰架構(Clean Architecture)的Go微服務: 程序結構

8 Go kit

9 Go Micro

相關文章
相關標籤/搜索