很久沒來這裏了,今天登上來發現界面風格變化很大,並且博客也開始支持markdown,愈來愈先進了。那麼就藉着這個勢頭分享下最近一直在研究的DDD-CQRS。html
有些人說「CQRS很難嗎?」redis
其實它和微服務結合起來纔是王道!數據庫
是嗎?好吧,我以前也一直是這麼認爲的!可是當我開始編寫使用CQRS的第一件軟件做品時,一切都改變了。我發現他一點都不復雜。並且我認爲在大型團隊中保持這種編程行爲將變得更容易。編程
我也曾思考爲何人們廣泛認爲CQRS很難,後來我想到是爲何了。由於有不少規則!人們討厭規則,現實社會中已經不少了,因此不少人沉迷於虛擬世界之中。規則讓咱們不舒服,由於咱們必須遵照它。在這篇博文中,我將從幾方面論證這些規則是容易理解的。緩存
通常來講,咱們認爲CQRS是讀寫分離架構的一種實現方式。當我以這種方法來理解的時候我發如今簡單的CQS實現和真實的CQRS之間仍是有幾個很重要的步驟須要區分對待的。這些步驟介紹了我以前提到的規則。markdown
咱們的旅程從下面這幅圖開始:網絡
上面是一張咱們都很熟悉的經典N層架構圖。若是往其中加入一些CQS的話,咱們就簡單地實現了將業務邏輯分離爲命令和查詢:session
若是你在維護一箇舊系統這多是最難的一步(由於老代碼基本上都是意大利麪條似的)。同時這一步也多是最有效果的,由於你從中對本身的代碼有了一個總體的梗概。架構
下面先來介紹下命令(Command)和查詢(Query)的定義吧!框架
首先,發送命令是惟一改變系統狀態的方式。Commands對全部系統的變動都要負責。若是沒有command,系統的狀態就一直保持不變!Command不該該返回任何值。我使用兩個類來實現:Command和CommandHandler。Command僅僅是被CommandHandler是用來做爲一些操做的輸入參數。在我這裏,command僅僅是領域模型中調用的一系列特定操做。
query至關於讀操做。經過讀取系統的狀態,過濾,聚合,轉換數據,最後以某種特定的格式傳輸。它能夠被執行屢次而且不會影響到系統的狀態。我一般在一個類中使用多個execute(...)方法來實現,但我如今卻認爲將Query和QueryHandler、QueryExecutor分開來說或許是對的。
回到上面的那張圖,我須要澄清一些事;我私自加進去了額外的變化,就是Model變成了Domain Model。model表明了數據的容器,而domain model則包含了複雜的商業邏輯。這點變化對於咱們感興趣的架構來講沒什麼直接的影響,可是值得注意的是因爲command接管了修改系統狀態的職責,主要的複雜性都集中在了Domain Model中。
稍後你就會發現Domain Model對於寫模型來講頗有用,可是對於讀來說卻表現不是很好。
咱們可使用這種分離模型,經過ORM映射來構造查詢,可是在某些場景,尤爲是當ORM遇到負載失衡的時候,下面這個架構可能會更合適:
如今咱們成功地在邏輯層面分離了Command和Query,可是他們還在共享同一個數據庫。這意味着實際上讀模型在使用的是DB的物化視圖(也可經過數據庫層面的讀寫分離代替視圖)。當咱們的系統不須要解決性能問題,並且咱們記得在寫模型變動的時候更新咱們的查詢的時候,這個解決方案還能夠。
下一步是介紹完整的分離數據模型:
CQRS != Event Sourcing
Event Sourcing(事件溯源)常常伴隨着CQRS出現。ES的的定義很簡單:咱們的領域產生的事件即代表了系統中發生過的變化。若是咱們從剛開始就記錄了整個系統並進行回放,咱們就會拿到當初系統的狀態記錄。能夠想象下銀行帳戶,從剛開始的空帳戶,經過回放每一個單一事務獲得最終(也就是目前)的收支狀況。因此,只要咱們存儲了全部的事件,就能獲得系統當前的狀態。
可是對於CQRS來講,領域模型究竟是怎樣存儲的不是特別重要,ES僅僅是其中的一個選項。(也可使用in-memory或mongo,redis等方式)
寫模型
因爲寫模型定義了領域模型的主要職責,而且作了不少商業決定,它是系統的心臟。它體現了系統的真實狀態,這些狀態能夠用來作出有意義的決定。
讀模型
剛開始我使用寫模型來構建湊合的查詢,在不斷的嘗試以後,咱們發現這很費時間。由於咱們是工程師,優化是第二需求。咱們設計的模型在讀取時將時間消耗在了關聯查詢上。咱們被迫預先計算出一些有報表需求的數據,這樣會使它在查詢時表現得很快。這頗有趣,由於咱們在這裏使用到了緩存。依我看來這是對讀模型最好的詮釋:它就是一個合理存在的緩存。緩存在這裏沒有細講,是由於咱們尚未觸及到項目的發佈,非功能性需求不該該過早設計。
事實上,讀模型能夠設計的很複雜,你可使用圖數據庫(相似Neoj)來存儲社交網絡,而使用RDBMS(關係型數據庫)來存儲財務數據。
設計良好的讀模型是須要必定的考量的。若是你的項目不是很大,寫模型已經能夠勝任幾乎全部的讀取需求,那麼設計個讀模型將會致使大量的拷貝代碼,這種作法無疑是在浪費時間。可是若是你的寫模型存儲了一系列的事件,那麼你將絕不費力得從中獲取到任什麼時候間點上的數據,而不用將事件從零開始進行回放。這個過程被稱做Eager Read Derivation,也是我認爲的在CQRS最複雜的一塊。讀模型即如我以前所說,是另外一種形式的緩存。而Phil Karlton曾說過「在計算機科學中只有兩個問題值得咱們關注:緩存的時效性和命名問題」
最終一致性
若是咱們的模型處於物理上隔離的情況,那麼同步就須要花費一些時間,可是這段時間對於某些業務來說是不能耽誤的。在個人項目中,若是全部的部分都運行得很正確,讀模型處於不一樣步的狀態是能夠忽略不計的。然而,咱們必須將時間上的差別性考慮進去尤爲是在開發更復雜的系統時。咱們設計的UI也是爲了可以及時的處理最終一致性。
咱們不得不認可即便在寫模型更新的時候讀模型也相應更新的狀況下,用戶也是難以接受老舊數據的出現。更況且咱們根部不肯定展示在用戶面前的數據是不是最新的。
下面我將要講述一些實際的代碼例子:
我是怎樣將CQRS引入個人項目中的?
我認爲CQRS是簡單的以致於不須要引入任何框架。你能夠從少於100行的代碼開始慢慢地實現它,當須要時再去擴展新功能。你不用作任何努力(學習新的技術),由於CQRS已經簡化了軟件開發。下面是個人實現:
public interface ICommand { } public interface ICommandHandler where TCommand : ICommand { void Execute(TCommand command); } public interface ICommandDispatcher { void Execute(TCommand command) where TCommand : ICommand; }
我定義了兩個接口用來描述命令和他的執行環境。這麼作是由於我想保持參數的扁平化,不想要任何依賴。個人命令處理器(command handler)可以從DI容器中請求依賴,根本沒有必要手動初始化它(除了在自測用例中)。實際上,ICommand接口出如今這裏無非是想告訴開發人員咱們把這個類當作command來用。
public interface IQuery { } public interface IQueryHandler where TQuery : IQuery { TResult Execute(TQuery query); } public interface Interface IQueryDispatcher { TResult Execute(TQuery query) where TQuery : IQuery; }
這裏講IQuery接口做爲返回的query結果類型。但這不是最優雅的方法,可是在編譯器類型被肯定了。
public class CommandDispatcher : ICommandDispatcher { private readonly IDependencyResolver _resolver; public CommandDispatcher(IDependencyResolver resolver) { _resolver = resolver; } public void Execute(TCommand command) where TCommand : ICommand { if(command == null) { throw new ArgumentNullException("command"); } var handler = _resolver.Resolver>(); if(handler == null) { throw new CommandHandlerNotFoundException(typeof(TCommand)); } handler.Execute(command); } }
這裏的CommandDispatcher相對來講是短小的,它只有一個職責就是爲command初始化合適的command handler並執行。爲了不手寫command註冊和初始化過程,我使用了DI容器來幫我作了。可是若是你不想使用DI容器你能夠本身來實現。我認爲這並不難。只有一個問題,那就是通用類型是比較難定義的,這在剛開始這麼作時可能有些挫敗感。但這種實現使用起來已經很簡單了,這裏是一個簡單的command和handler的例子:
public class SignOnCommand : ICommand { public AssignmentId Id { get; private set; } public LocalDateTime EffectiveDate { get; private set; } public SignOnCommand(AssignmentId assignmentId, LocalDateTime effectiveDate) { Id = assignmentId; EffectiveDate = effectiveDate; } } public class SignOnCommandHandler : ICommandHandler { private readonly AssignmentRepository _assignmentRepository; private readonly SignOnPolicyFactory _factory; public SignOnCommandHandler(AssignmentRepository assignmentRepository, SignOnPolicyFactory factory) { _assignmentRepository = assignmentRepository; _factory = factory; } public void Execute(SignOnCommand command) { var assignment = _assignmentRepository.GetById(command.Id); if(assignment == null) { throw new MeaningfulDomainException("Assignment not found!"); } var policy = _factory.GetPolicy(); assignment.SignOn(command.EffectiveDate, policy); } }
只須要將SignOnCommand傳入dispatcher就能夠了:
_commandDispatcher.Execute(new SignOnCommand(new AssignmentId(rawId), effectiveDate));
就是這麼簡單!惟一的區別就是它返回了指定的數據,依賴於以前定義的通用的Execute方法,返回了強類型的結果:
public class QueryDispatcher : IQueryDispatcher { private readonly IDependencyResolver _resolver; public QueryDispatcher(IDependencyResolver resolver) { _resolver = resolver; } public void Execute(TQuery query) where TQuery : IQuery { if(query == null) { throw new ArgumentNullException("query"); } var handler = _resolver.Resolver>(); if(handler == null) { throw new QueryHandlerNotFoundException(typeof(TQuery)); } handler.Execute(query); } }
這個實現是擴展性極強的。好比咱們想引入事務到comamnd dispatcher中,能夠像下面這樣作,無須動用任何原有的實現代碼:
public class TransactionalCommandDispatcher : ICommandDispatcher { private readonly ICommandDispatcher _next; private readonly ISessionFactory _sessionFactory ; public TransactionalCommandDispatcher(ICommandDispatcher next, ISessionFactory sessionFactory) { _next = next; _sessionFactory = sessionFactory; } public void Execute(TCommand command) where TCommand : ICommand { using(var session = _sessionFactory.GetSession()) using(var tx = session.BeginTransaction()) { try { _next.Execute(command); ex.Commit(); } catch { tx.Rollback(); throw; } } } }
經過使用這種「僞切面」,咱們能夠簡單地實現Command和Query dispatcher的擴展。
如今你明白了CQRS並不難,基本的觀點已經講的很清晰了,可是你仍是須要聽從一些規則。這篇博客沒有包含所有的你想知道的東西,因此我推薦你讀一讀下面這些文章。
參考文獻:
1 CQRS Documents by Greg Young
5 「Implementing DDD」 by Vaughn Vernon
原文地址:https://www.future-processing.pl/blog/cqrs-simple-architecture/