领域驱动设计实践下

领域驱动设计实践下篇

一、写在前面

  上篇大致介绍过了领域驱动的主要概念,内容并不详尽,相关方面的知识大家可以参考园子里汤雪华陈晴阳的博客,上篇有说过,领域驱动设计重点是建立正确的领域模型,这取决于对业务的理解和抽象能力,本篇将以一个简单的订单流程来实践领域驱动设计,希望能够给想实践DDD的人提供一种实现思路。

二、订单流程

  image

  这是一个简化了的订单流程,实际情况还有很多细节要考虑。但这不妨碍本文的一个演示目的。

  图中的发布事件即为发布消息至消息队列,为了达到EventSourcing的目的会在每次发布消息前将其持久化到数据库。

  示例源码在本文最下面。

三、搭建分层架构解决方案

  我们以领域驱动设计的经典分层架构来搭建我们的解决方案。如下图

  image

  Applicaiton:应用层,在这里我们用ServiceStack实现的Web服务来作为应用层(实际情况该层承担的应该是应用功能的划分和协调,但作为示例将其合并在同一层次)。

  Domain:领域层,包含了业务所涉及的领域对象(实体、值对象),技术无关性。

  Infrastructure:基础设施层,数据库持久化,消息队列实现,业务无关性。

  SampleTests:在这里我们用单元测来作为表现层。

四、基础设施层

  1:首先定义出领域模型

领域模型有一个聚合根的概念,定义模型之前我们先定义一个聚合根的接口。

  1. namespace Infrastructure.Database  
  2. {  
  3.     /// <summary>  
  4.     /// 聚合根  
  5.     /// </summary>  
  6.     public interface IAggregateRoot  
  7.     {  
  8.         /// <summary>  
  9.         //  每个聚合根必须拥有一个全局的唯一标识,往往是GUID。  
  10.         /// </summary>  
  11.         Guid Id { get; set; }   
  12.     }  
  13. }  

我们为该聚合根定义一个抽象的实现类,通过使用[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]可以使主键按排序规则生成。

  1. namespace Infrastructure.Database  
  2. {  
  3.     /// <summary>  
  4.     /// 实体基类  
  5.     /// </summary>  
  6.     public abstract class EntityBase<TKey>   
  7.     {  
  8.         [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]  
  9.         public TKey Id { get; set; }  
  10.     }  
  11.     /// <summary>  
  12.     /// 实体基类(GUID)  
  13.     /// </summary>  
  14.     public abstract class EntityBase :EntityBase<Guid>, IAggregateRoot  
  15.     {  
  16.     }  
  17. }  

  2:CQRS接口及定义

首先是Command

  1. namespace Infrastructure.Commands  
  2. {  
  3.     public interface ICommand : IReturn<CommandResult>  
  4.     {  
  5.         Guid CommandId { get; }  
  6.     }  
  7. }  
  8. namespace Infrastructure.Commands  
  9. {  
  10.     public interface ICommandHandler<in TCommand> : IHandler<TCommand>  
  11.        where TCommand : ICommand  
  12.     {  
  13.     }  
  14. }  
  15. namespace Infrastructure.Commands  
  16. {  
  17.     public class CommandResult  
  18.     {  
  19.         public CommandResult()  
  20.         {  
  21.         }  
  22.   
  23.         public CommandResult(bool result = true,string msg = "")  
  24.         {  
  25.             this.Result = result;  
  26.             this.Msg = msg;  
  27.         }  
  28.   
  29.         public bool Result { get; set; }  
  30.         public string Msg { get; set; }   
  31.     }  
  32. }  
  33. namespace Infrastructure.Commands  
  34. {  
  35.     public abstract class CommandHandlerBase  
  36.     {  
  37.         protected async Task DoHandle<TMessage>(Func<TMessage, Task> handlerAction, TMessage message) where TMessage : ICommand  
  38.         {  
  39.             try  
  40.             {  
  41.                 await handlerAction.Invoke(message);  
  42.             }  
  43.             //catch MoreException  
  44.             catch (Exception e)  
  45.             {  
  46.                 throw new Exception(e.Message);  
  47.             }  
  48.         }  
  49.     }  
  50. }  

然后是Event

  1. namespace Infrastructure.Events  
  2. {  
  3.     public interface IEvent : IReturnVoid  
  4.     {  
  5.         Guid EventId { get; }   
  6.     }  
  7. }  
  8. namespace Infrastructure.Events  
  9. {  
  10.     public interface IEventHandler<in TEvent> : IHandler<TEvent>  
  11.           where TEvent : IEvent  
  12.     {  
  13.     }  
  14. }  
  15. namespace Infrastructure.Events  
  16. {  
  17.     public abstract class EventHandlerBase  
  18.     {  
  19.         public virtual async Task DoHandle<TMessage>(Func<TMessage, Task> handlerAction, TMessage message) where TMessage : IEvent  
  20.         {  
  21.             try  
  22.             {  
  23.                 await handlerAction.Invoke(message);  
  24.             }  
  25.             //catch MoreException  
  26.             catch (Exception e)  
  27.             {  
  28.                 throw new Exception(e.Message);  
  29.             }  
  30.         }  
  31.     }  
  32. }  

最后是Bus

  1. namespace Infrastructure.Bus  
  2. {  
  3.     public interface IEventBus   
  4.     {  
  5.         void Publish<T>(T message) where T : IEvent;  
  6.     }  
  7. }  
  8. namespace Infrastructure.Bus  
  9. {  
  10.     public interface ICommandBus  
  11.     {  
  12.         CommandResult Excute<T>(T command) where T : ICommand;  
  13.         Task<CommandResult> ExcuteAsync<T>(T command) where T : ICommand;  
  14.     }  
  15. }  

基础设施层到这里就算完成了,还有个仓储的实现上篇有说明,有点要说明的是本文的示例Domain层中并没有做到真正的纯净,譬如数据库持久化采用的EF实现,且把上下文放置在了Domain层,若作为开发框架是不建议这样做的,要达到完全解耦可以参考陈晴阳的开源项目Apworks

五、领域层

  首先定义出所需要的领域模型

  1. namespace Domain.Entitys  
  2. {  
  3.     /// <summary>  
  4.     /// 订单实体类  
  5.     /// </summary>  
  6.     public class Order : EntityBase  
  7.     {  
  8.         public string OrderNo { get; set; }  
  9.         public decimal OrderAmount { get; set; }  
  10.         public DateTime OrderTime { get; set; }  
  11.         public string ProductNo { get; set; }  
  12.         public string UserIdentifier { get; set; }  
  13.         public bool IsPaid { get; set; }  
  14.     }  
  15.     /// <summary>  
  16.     /// 订单支付实体类  
  17.     /// </summary>  
  18.     public partial class PayOrder : EntityBase  
  19.     {  
  20.         public decimal PayAmount { get; set; }  
  21.         public string PayResult { get; set; }  
  22.         public string OrderNo { get; set; }  
  23.     }  
  24. }  

此处订单模型继承抽象类,如此可以保持模型的纯净,你甚至可以根据业务差异性定义多个基类,通常我们会将通用的一些属性及方法定义在基类中,如IsDeleted【逻辑删除】、CreateTime、Timestamp【并发控制】等。

为简化流程示例中仅包含两个操作,【生成订单】和【支付订单】,我们将其定义在领域服务内。

  1. namespace Domain.DomainServices  
  2. {  
  3.     public interface IOrderService  
  4.     {  
  5.         Task OrderBuild(Order order);  
  6.   
  7.         Task Pay(Order order);  
  8.     }  
  9. }  
  10. namespace Domain.DomainServices  
  11. {  
  12.     public class OrderService : IOrderService  
  13.     {  
  14.         public IRepository<Order> OrderRepository { private get; set; }  
  15.         public IRepository<PayOrder> PayOrderRepository { private get; set; }  
  16.         public IRepository<EventStore> EventStoreRepository { private get; set; }  
  17.         public IEventBus EventBus { private get; set; }  
  18.   
  19.         public async Task OrderBuild(Order order)  
  20.         {  
  21.             //生成订单  
  22.             await OrderRepository.AddAsync(order);  
  23.             //toEventStore  
  24.             await EventStoreRepository.AddAsync(order.ToBuildOrderReadyEvent().ToEventStore());  
  25.             //发布生成订单事件  
  26.             EventBus.Publish(order.ToBuildOrderReadyEvent());  
  27.         }  
  28.   
  29.         public async Task Pay(Order order)  
  30.         {  
  31.             var payOrder = new PayOrder  
  32.             {  
  33.                 OrderNo = order.OrderNo,  
  34.                 PayAmount = order.OrderAmount,  
  35.                 PayResult = "pay success!"  
  36.             };  
  37.             //支付成功  
  38.             await PayOrderRepository.AddAsync(payOrder);  
  39.             //更新订单  
  40.             var findOrder = await OrderRepository.GetByKeyAsync(order.Id);  
  41.             findOrder.IsPaid = true;  
  42.             await OrderRepository.UpdateAsync(findOrder);  
  43.             //toEventStore  
  44.             await EventStoreRepository.AddAsync(payOrder.ToPaySuccessReadyEvent().ToEventStore());  
  45.             //发布支付成功事件  
  46.             EventBus.Publish(payOrder.ToPaySuccessReadyEvent());  
  47.         }  
  48.     }  
  49. }  

要驱动整个流程的订单发起,我们需要定义一个Command【OrderBuild】,它通常是根据调用端数据DTO转化而来。

  1. namespace Domain.Commands  
  2. {  
  3.      [Route("/BuildOrder", "Post")]  
  4.     public class BuildOrder : Command  
  5.     {  
  6.         public string OrderNo { get; set; }  
  7.         public decimal OrderAmount { get; set; }  
  8.         public string ProductNo { get; set; }  
  9.         public string UserIdentifier { get; set; }  
  10.   
  11.         public Order ToOrder()  
  12.         {  
  13.             return new Order  
  14.             {  
  15.                 OrderNo = OrderNo,  
  16.                 OrderAmount = OrderAmount,  
  17.                 OrderTime = DateTime.Now,  
  18.                 ProductNo = ProductNo,  
  19.                 UserIdentifier = UserIdentifier,  
  20.                 IsPaid = false  
  21.             };  
  22.         }  
  23.     }  
  24. }  

有了Command,接着定义出该命令的处理程序

  1. namespace Domain.Commands.Handlers  
  2. {  
  3.     public class OrderCommandHandler :CommandHandlerBase,  
  4.         ICommandHandler<BuildOrder>  
  5.     {  
  6.         public IOrderService OrderService { private get; set; }  
  7.   
  8.         public async Task Handle(BuildOrder command)  
  9.         {  
  10.             await DoHandle(async c => { await OrderService.OrderBuild(command.ToOrder()); }, command);  
  11.         }  
  12.     }  
  13. }  

由上面定义的领域服务中可见,OrderBuild和Pay中都发布有事件,事件及其处理程序如下

  1. namespace Domain.Events  
  2. {  
  3.     public class BuildOrderReady : Event  
  4.     {  
  5.         public Order Entity { get; set; }  
  6.   
  7.         public EventStore ToEventStore()  
  8.         {  
  9.             return new EventStore  
  10.             {  
  11.                 Timestamp = DateTime.Now,  
  12.                 Body = JsonConvert.SerializeObject(Entity)  
  13.             };  
  14.         }  
  15.     }  
  16. }  
  17. namespace Domain.Events  
  18. {  
  19.     public class PaySuccessReady : Event  
  20.     {  
  21.         public PayOrder Entity { get; set; }  
  22.   
  23.         public EventStore ToEventStore()  
  24.         {  
  25.             return new EventStore  
  26.             {  
  27.                 Timestamp = DateTime.Now,  
  28.                 Body = JsonConvert.SerializeObject(Entity)  
  29.             };  
  30.         }  
  31.     }  
  32. }  
  33. namespace Domain.Events.Handlers  
  34. {  
  35.     public class OrderEventHandler : EventHandlerBase,  
  36.         IEventHandler<BuildOrderReady>,  
  37.         IEventHandler<PaySuccessReady>  
  38.     {  
  39.         public IOrderService OrderService { private get; set; }  
  40.         public async Task Handle(BuildOrderReady @event)  
  41.         {  
  42.             await DoHandle(async c => { await OrderService.Pay(@event.Entity); }, @event);  
  43.         }  
  44.   
  45.         public async Task Handle(PaySuccessReady @event)  
  46.         {  
  47.             //Send Email..  
  48.             //Send SMS..  
  49.         }  
  50.     }  
  51. }  

可以看到在两个Event中都包含有ToEventStore方法,此处仅为模拟出将当前Event序列化保存,以供EventSourcing使用。这里有较成熟的框架可以使用,如NEventStorehttp://geteventstore.com/

六、应用层

  开头有说过应用层采用ServiceStack实现的Web服务,优点有3

  1:ServiceStack强调数据交换需定义出RequestDto及ResponseDto,这很符合我们CQRS的一个Command机制

  2:示例中Event即消息,Publish的Event将在MQ中,通过订阅去消费,示例采用的消息队列是RabbitMq(跨平台),这样一来可以使用其他平台的语言去订阅该消息并消费,ServiceStack将Rabbitmq的部分功能集成在内。

  3:其实是第二点的衍生,当事件经过MQ,有些消息我们可以消费即ACK掉,有些消息我们可以将其存储在队列中,如此一来我们可以基于订阅MQ来实现系统对业务的一个分析和数据处理,如下图

image

  ServiceStack中服务的定义只需要继承ServiceStack.Service或者IService,如下

  1. namespace Application.Services  
  2. {  
  3.     public partial class CommandService : Service  
  4.     {  
  5.         public async Task<CommandResult> Any(BuildOrder command)  
  6.         {  
  7.             return await Handler(command);  
  8.         }  
  9.     }  
  10. }  
  11. namespace Application.Services  
  12. {  
  13.     public partial class EventService : Service  
  14.     {  
  15.         public async Task Any(BuildOrderReady @event)  
  16.         {  
  17.             await Handler(@event);  
  18.         }  
  19.   
  20.         public async Task Any(PaySuccessReady @event)  
  21.         {  
  22.             await Handler(@event);  
  23.         }  
  24.     }  
  25. }  

关于方法名定义成Any是推荐的做法,若要控制其Post或Get等可以在其RequestDto上以 [Route("/BuildOrder", "Post")]标签的形式拓展。

因ServiceStack要求RequestDto定义的同时须要指定其ResponseDto,以继承IReturn<ResponseDto>接口来声明。

示例中我们的Command都是继承自IReturn<CommandResult>,Event都是继承自IReturnVoid,如下

  1. namespace Infrastructure.Commands  
  2. {  
  3.     public interface ICommand : IReturn<CommandResult>  
  4.     {  
  5.         Guid CommandId { get; }  
  6.     }  
  7. }  
  8. namespace Infrastructure.Events  
  9. {  
  10.     public interface IEvent : IReturnVoid  
  11.     {  
  12.         Guid EventId { get; }   
  13.     }  
  14. }  

在ServiceStack中需要定义一个继承自AppHostBase的服务宿主类(姑且这样叫吧),通常取名叫AppHost,如下

  1. namespace Application.Services.Config  
  2. {  
  3.     public class AppHost : AppHostBase  
  4.     {  
  5.         public AppHost()  
  6.             : base("CQRS Demo", typeof(AppHost).Assembly) { }  
  7.   
  8.         public override void Configure(Funq.Container container)  
  9.         {  
  10.             //SwaggerUI配置用于调试  
  11.             AddPlugin(new SwaggerFeature());  
  12.   
  13.             //IOC配置  
  14.             ServiceLocatorConfig.Configura(container);  
  15.   
  16.             //rabbitmq配置  
  17.             var mq = new RabbitMqServer(ConfigurationManager.AppSettings.Get("EventProcessorAddress"))  
  18.             {  
  19.                 AutoReconnect = true,  
  20.                 DisablePriorityQueues = true,  
  21.                 RetryCount = 0  
  22.             };  
  23.             container.Register<IMessageService>(c => mq);  
  24.             var mqServer = container.Resolve<IMessageService>();  
  25.   
  26.             //注册eventHandler  
  27.             mq.RegisterHandler<BuildOrderReady>(ServiceController.ExecuteMessage, 1);  
  28.             mq.RegisterHandler<PaySuccessReady>(ServiceController.ExecuteMessage, 1);  
  29.   
  30.             mqServer.Start();  
  31.         }  
  32.     }  
  33. }  

构造函数中的两个参数分别代表,服务显示名称和指定当前服务定义所在的程序集。

SwaggerUI用于调试服务接口是非常方便的,内置的依赖注入框架Funq功能也不错。

另外就是rabbitmq的使用需要在Nuget中另外安装,全称是:ServiceStack.RabbitMq。值得一提的是服务启动时,ServiceStack会在你指定的Rabbitmq服务端创建对应的队列,通常是根据你定义的Event创建如下。

image

可以看到每个Event创建了4个队列,分别代表的意思是:

dlq:没有对应的处理程序或处理失败的消息。

inq:还未被消费的消息。

outq:处理完毕的消息。

priorityq:优先队列。

更详细的可以到ServiceStack Wiki上查看。

优点:在注册EventHandler时可以指定处理线程个数,如上面指定的是1,此时若同样的服务有两个,分别部署在不同服务器上且都订阅相同消息时,将根据线程数来消费MQ中的消息来达到负载均衡的目的。

  1. //注册eventHandler  
  2. mq.RegisterHandler<BuildOrderReady>(ServiceController.ExecuteMessage, 1);  
  3. mq.RegisterHandler<PaySuccessReady>(ServiceController.ExecuteMessage, 1);  

但其实针对Rabbitmq封装一个Client并不麻烦,我们可以按项目需要去实现其Exchanges和Queues,并且可以很灵活的控制Ack等。

七、调试

  在单元测试中我们按如下方式调试。

image

也可以使用SwaggerUI调试,服务运行之后将打开如下页面

image

点击SwaggerUI打开调试页面

image

点击Try it out!按钮

image

此时数据库中应包含一条Order记录一条PayOrder记录和两条EventStore记录

image

RabbitMq中

image

八、源码

  源码地址:https://github.com/yanghongjie/DomainDrivenDesignSample

 
分类: DDD
原文地址:https://www.cnblogs.com/Leo_wl/p/4232424.html