zoukankan      html  css  js  c++  java
  • 领域驱动设计实践下

    领域驱动设计实践下篇

    一、写在前面

      上篇大致介绍过了领域驱动的主要概念,内容并不详尽,相关方面的知识大家可以参考园子里汤雪华陈晴阳的博客,上篇有说过,领域驱动设计重点是建立正确的领域模型,这取决于对业务的理解和抽象能力,本篇将以一个简单的订单流程来实践领域驱动设计,希望能够给想实践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
  • 相关阅读:
    Windows Server 2016-Active Directory复制概念(二)
    Windows Server 2016-Active Directory复制概念(一)
    Windows Server 2016-Wbadmin命令行备份域控制器
    Windows Server 2016-图形化备份域控制器
    Windows Server 2016-Nano Server介绍
    Windows Server 2016-系统安装软硬件要求
    每天一个linux命令(51)--grep命令
    每天一个linux命令(50)--date命令
    每天一个linux命令(49)--diff命令
    每天一个linux命令(48)--ln命令
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/4232424.html
Copyright © 2011-2022 走看看