IDDD 實現領域驅動設計-一個簡單的 CQRS 示例


上一篇:《IDDD 實現領域驅動設計-CQRS(命令查詢職責分離)和 EDA(事件驅動架構)

學習架構知識,需要有一些功底和經驗,要不然你會和我一樣吃力,CQRS、EDA、ES、Saga 等等,這些是實踐 DDD 所必不可少的架構,所以,如果你不懂這些,是很難看懂上篇所提到的 CQRS Journey 和 ENode 項目,那怎么辦呢?我們可以從簡單的 Demo 一點一滴開始。

代碼地址:https://github.com/yuezhongxin/CQRS.Sample


說明:一張很丑陋的圖

CQRS.Sample 所描述的一個流程是 SendMessage(發消息),也就是之前 MessageManager 中的一個業務示例,這個業務流程用到 CQRS 示例中,可能會有些不太准確,或者是有些牽強,但我主要的目的是想做一次 Commond->Event 的過程,熟悉它們到底是怎么交互的?所以,你查看代碼的時候,盡量忽略這個業務流程,當然,如果你有針對這個業務流程更好的具體應用,我是非常歡迎交流。

首先,我們根據上面的流程圖一步一步進行,UI 創建一個 Commond,然后交給 CommandBus 進行 Send(發送),也就是下面這段代碼:

var commond = new SendMessageCommond()
{
    Title = "this is title",
    Content = "this is content",
    SenderLoginName = "this is senderLoginName",
    RecipientDisplayName = "this is recipientDisplayName"
};
var commandBus = IocContainer.Default.Resolve<CommandBus>();
commandBus.Send(commond);

項目中所有的類型映射都是通過 IoC 進行注入,ICommandBus 接口定義為:void Send<TCommand>(TCommand cmd) where TCommand : ICommand;,CommandBus 的實現主要是在 Send 方法中,解析 ICommandHandler<TCommand> 注入的類型對象,然后調用 ICommandHandler 接口定義的 Handle 方法,傳入 TCommand 參數對象。

SendMessageCommond 的定義很簡單,主要是一些來自 UI 的參數,所以,我們一般會定義一些屬性字段,有時候我們會進行數據驗證,可以使用 Validate,方法簽名是: IEnumerable<ValidationResult> Validate(ValidationContext validationContext),不過需要繼承 IValidatableObject 接口,這樣我們就可以在 MVC View 前端進行輸出驗證結果,使用起來非常方便,SendMessageCommond 繼承一個無實現的 ICommand 接口,主要用來約束類型,SendMessageCommond 對應 SendMessageCommondHandler,實現代碼:

public class SendMessageCommondHandler : 
    ICommandHandler<SendMessageCommond>
{
    public void Handle(SendMessageCommond command)
    {
        var sender = VerifySenderService.Verify(command.SenderLoginName);
        var receiver = VerifyReceiverService.Verify(command.RecipientDisplayName);
        var message = new Message(command.Title, command.Content, sender, receiver);
        message.Send();
    }
}

CommondHandler 的功能有點類似於經典分層架構中的 Application(應用層),從它的具體實現中,我們就可以看出領域到底在做哪些工作,它的主要工作就是協調這些工作的流程,領域中的代碼我就不貼了,這里我簡單說明一下,在之前的 SendMessage 實現中,設計了一個 SendMessageService 領域服務,里面主要進行的工作是驗證收發件人,以及消息是否符合規則,后來我就想,SendMessageService 和它實際進行的工作不相符,發消息所蘊含的實際業務意義,我也一直沒有想明白,但是在具體實現中,發消息所做的工作是驗證,那驗證是不是發消息真正的業務含義呢?所以,稀里糊塗就有了上面的代碼,VerifySenderService 和 VerifyReceiverService 是用來驗證收發件人信息的,成功的話就返回一個 Contact 對象,具體的驗證邏輯可以查看下實現代碼。

下面說一下 message.Send();,這個可能有很大的問題,在 CQRS Journey 項目中,有很多類似於這樣的操作,就是在 CommondHandler 中,創建一個聚合根對象,然后執行聚合根中的一個行為,比如我搜刮的 order.Confirm(),訂單可以提交自己嗎?消息可以發送自己嗎?這樣做的含義是什么?其實我也搞不太清楚,在 Message 聚合根中的 Send 方法中,主要是事件的發布,

public void Send()
{
    eventBus.Publish(new MessageSentEvent(this));
}

先拋開 Send 的合理性,看下 EventBus 是如何 Publish 的?我的實現中和 CommondBus 很相似,但我覺得可能有些問題,Commond 和 CommondHandler 是一一對應關系,我們知道事件發布和事件訂閱是一對多關系,也就是說一個事件可能有很對的訂閱者,這些訂閱者所處理的過程可能會有些不同,比如用戶注冊的一個事件,可能會有郵件通知訂閱者,也可能會有統計數據更新訂閱者,在 CQRS Journey 項目中,EventBus 的 Publish 大概是這樣實現的:

public void Publish(Envelope<IEvent> @event)
{
    var message = this.BuildMessage(@event);

    this.sender.Send(message);
}
private Message BuildMessage(Envelope<IEvent> @event)
{
    using (var payloadWriter = new StringWriter())
    {
        this.serializer.Serialize(payloadWriter, @event.Body);
        return new Message(payloadWriter.ToString(), correlationId: @event.CorrelationId);
    }
}

而我的實現則是和 CommondBus 一樣,都是用 IoC 注入的,所以肯定有問題,我們來看下 MessageSentEventHandler 中的代碼:

public class MessageSentEventHandler : 
    IEventHandler<MessageSentEvent>,
    IEventHandler<MailSentEvent>
{
    private IEventBus eventBus;

    public void Handle(MessageSentEvent @event)
    {
        eventBus = IocContainer.Default.Resolve<IEventBus>();
        new MessageRepository().Save<Message>(@event.Message);
        eventBus.Publish(new MailSentEvent
        {
            Title = @event.Message.Title,
            Content = @event.Message.Title,
        });
    }

    public void Handle(MailSentEvent @event)
    {
        var mailMessage = new System.Net.Mail.MailMessage();
        mailMessage.Subject = @event.Title;
        mailMessage.Body = @event.Content;
        mailMessage.IsBodyHtml = true;
        mailMessage.BodyEncoding = System.Text.Encoding.UTF8;
        mailMessage.Priority = System.Net.Mail.MailPriority.Normal;
        System.Net.Mail.SmtpClient smtpClient = new System.Net.Mail.SmtpClient();
        Task.Run(() => { smtpClient.SendAsync(mailMessage, mailMessage.Body); });
    }
}

消息發送之后,進行持久化操作,然后再進行郵件通知,這樣一整個 SendMessage 的流程就走完了。

這個流程中,只有 CQRS 的 Commond,並沒有 Query,也沒有 ES、Saga 的概念,主要是它們太深奧了,我搞不來。CQRS.Sample 只是一個簡單的示例,發消息的業務含義所表達的可能不是很准確,本來是想用用戶注冊、訂單提交業務示例,但還是想想用發消息進行嘗試下,除去 EventBus 的實現有問題,CQRS 的簡化版基本上都能表現出來了。

另外,從簡單分層架構改造成 CQRS 確實有很多挑戰,但有時候想想,領域模型都設計的有問題,那用什么架構實現都毫無意義,如果在現有的項目中,你發現經典分層架構實現起來有很多別扭的地方,比如 Domain 引用了 DTO,你可以嘗試先把 Repositories 進行分離下,創建一個 Query 項目,把一些無業務邏輯的查詢發到里面(主要是應用層調用的),這樣使 Repositories 更加簡化,如果可以簡化成只有一個 GetById 方法,那么就達到 CQRS 的標准了,因為 Repositories 的接口定義在領域層,因為 Query 項目的分離,所以,Domain 就可以去除 DTO 的引用了,應用層也就直接調用 Query,這只是一個中和方案。

領域模型需要一點一滴設計,架構也需要一點一滴設計,但后者需要建立在前者的基礎上。

一個對我非常有幫助的 CQRS 系列(初級):


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM