• 领域驱动设计实践下篇


    一、写在前面

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

    二、订单流程

      image

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

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

      示例源码在本文最下面。

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

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

      image

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

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

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

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

    四、基础设施层

      1:首先定义出领域模型

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

    namespace Infrastructure.Database
    {
        /// <summary>
        /// 聚合根
        /// </summary>
        public interface IAggregateRoot
        {
            /// <summary>
            //  每个聚合根必须拥有一个全局的唯一标识,往往是GUID。
            /// </summary>
            Guid Id { get; set; } 
        }
    }

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

    namespace Infrastructure.Database
    {
        /// <summary>
        /// 实体基类
        /// </summary>
        public abstract class EntityBase<TKey> 
        {
            [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public TKey Id { get; set; }
        }
        /// <summary>
        /// 实体基类(GUID)
        /// </summary>
        public abstract class EntityBase :EntityBase<Guid>, IAggregateRoot
        {
        }
    }

      2:CQRS接口及定义

    首先是Command

    namespace Infrastructure.Commands
    {
        public interface ICommand : IReturn<CommandResult>
        {
            Guid CommandId { get; }
        }
    }
    namespace Infrastructure.Commands
    {
        public interface ICommandHandler<in TCommand> : IHandler<TCommand>
           where TCommand : ICommand
        {
        }
    }
    namespace Infrastructure.Commands
    {
        public class CommandResult
        {
            public CommandResult()
            {
            }
    
            public CommandResult(bool result = true,string msg = "")
            {
                this.Result = result;
                this.Msg = msg;
            }
    
            public bool Result { get; set; }
            public string Msg { get; set; } 
        }
    }
    namespace Infrastructure.Commands
    {
        public abstract class CommandHandlerBase
        {
            protected async Task DoHandle<TMessage>(Func<TMessage, Task> handlerAction, TMessage message) where TMessage : ICommand
            {
                try
                {
                    await handlerAction.Invoke(message);
                }
                //catch MoreException
                catch (Exception e)
                {
                    throw new Exception(e.Message);
                }
            }
        }
    }

    然后是Event

    namespace Infrastructure.Events
    {
        public interface IEvent : IReturnVoid
        {
            Guid EventId { get; } 
        }
    }
    namespace Infrastructure.Events
    {
        public interface IEventHandler<in TEvent> : IHandler<TEvent>
              where TEvent : IEvent
        {
        }
    }
    namespace Infrastructure.Events
    {
        public abstract class EventHandlerBase
        {
            public virtual async Task DoHandle<TMessage>(Func<TMessage, Task> handlerAction, TMessage message) where TMessage : IEvent
            {
                try
                {
                    await handlerAction.Invoke(message);
                }
                //catch MoreException
                catch (Exception e)
                {
                    throw new Exception(e.Message);
                }
            }
        }
    }

    最后是Bus

    namespace Infrastructure.Bus
    {
        public interface IEventBus 
        {
            void Publish<T>(T message) where T : IEvent;
        }
    }
    namespace Infrastructure.Bus
    {
        public interface ICommandBus
        {
            CommandResult Excute<T>(T command) where T : ICommand;
            Task<CommandResult> ExcuteAsync<T>(T command) where T : ICommand;
        }
    }

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

    五、领域层

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

    namespace Domain.Entitys
    {
        /// <summary>
        /// 订单实体类
        /// </summary>
        public class Order : EntityBase
        {
            public string OrderNo { get; set; }
            public decimal OrderAmount { get; set; }
            public DateTime OrderTime { get; set; }
            public string ProductNo { get; set; }
            public string UserIdentifier { get; set; }
    		public bool IsPaid { get; set; }
        }
        /// <summary>
        /// 订单支付实体类
        /// </summary>
    	public partial class PayOrder : EntityBase
        {
            public decimal PayAmount { get; set; }
            public string PayResult { get; set; }
            public string OrderNo { get; set; }
        }
    }

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

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

    namespace Domain.DomainServices
    {
        public interface IOrderService
        {
            Task OrderBuild(Order order);
    
            Task Pay(Order order);
        }
    }
    namespace Domain.DomainServices
    {
        public class OrderService : IOrderService
        {
            public IRepository<Order> OrderRepository { private get; set; }
            public IRepository<PayOrder> PayOrderRepository { private get; set; }
            public IRepository<EventStore> EventStoreRepository { private get; set; }
            public IEventBus EventBus { private get; set; }
    
            public async Task OrderBuild(Order order)
            {
                //生成订单
                await OrderRepository.AddAsync(order);
                //toEventStore
                await EventStoreRepository.AddAsync(order.ToBuildOrderReadyEvent().ToEventStore());
                //发布生成订单事件
                EventBus.Publish(order.ToBuildOrderReadyEvent());
            }
    
            public async Task Pay(Order order)
            {
                var payOrder = new PayOrder
                {
                    OrderNo = order.OrderNo,
                    PayAmount = order.OrderAmount,
                    PayResult = "pay success!"
                };
                //支付成功
                await PayOrderRepository.AddAsync(payOrder);
                //更新订单
                var findOrder = await OrderRepository.GetByKeyAsync(order.Id);
                findOrder.IsPaid = true;
                await OrderRepository.UpdateAsync(findOrder);
                //toEventStore
                await EventStoreRepository.AddAsync(payOrder.ToPaySuccessReadyEvent().ToEventStore());
                //发布支付成功事件
                EventBus.Publish(payOrder.ToPaySuccessReadyEvent());
            }
        }
    }

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

    namespace Domain.Commands
    {
         [Route("/BuildOrder", "Post")]
        public class BuildOrder : Command
        {
            public string OrderNo { get; set; }
            public decimal OrderAmount { get; set; }
            public string ProductNo { get; set; }
            public string UserIdentifier { get; set; }
    
            public Order ToOrder()
            {
                return new Order
                {
                    OrderNo = OrderNo,
                    OrderAmount = OrderAmount,
                    OrderTime = DateTime.Now,
                    ProductNo = ProductNo,
                    UserIdentifier = UserIdentifier,
                    IsPaid = false
                };
            }
        }
    }

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

    namespace Domain.Commands.Handlers
    {
        public class OrderCommandHandler :CommandHandlerBase,
            ICommandHandler<BuildOrder>
        {
            public IOrderService OrderService { private get; set; }
    
            public async Task Handle(BuildOrder command)
            {
                await DoHandle(async c => { await OrderService.OrderBuild(command.ToOrder()); }, command);
            }
        }
    }

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

    namespace Domain.Events
    {
        public class BuildOrderReady : Event
        {
            public Order Entity { get; set; }
    
            public EventStore ToEventStore()
            {
                return new EventStore
                {
                    Timestamp = DateTime.Now,
                    Body = JsonConvert.SerializeObject(Entity)
                };
            }
        }
    }
    namespace Domain.Events
    {
        public class PaySuccessReady : Event
        {
            public PayOrder Entity { get; set; }
    
            public EventStore ToEventStore()
            {
                return new EventStore
                {
                    Timestamp = DateTime.Now,
                    Body = JsonConvert.SerializeObject(Entity)
                };
            }
        }
    }
    namespace Domain.Events.Handlers
    {
        public class OrderEventHandler : EventHandlerBase,
            IEventHandler<BuildOrderReady>,
            IEventHandler<PaySuccessReady>
        {
            public IOrderService OrderService { private get; set; }
            public async Task Handle(BuildOrderReady @event)
            {
                await DoHandle(async c => { await OrderService.Pay(@event.Entity); }, @event);
            }
    
            public async Task Handle(PaySuccessReady @event)
            {
                //Send Email..
                //Send SMS..
            }
        }
    }

    可以看到在两个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,如下

    namespace Application.Services
    {
        public partial class CommandService : Service
        {
            public async Task<CommandResult> Any(BuildOrder command)
            {
                return await Handler(command);
            }
        }
    }
    namespace Application.Services
    {
        public partial class EventService : Service
        {
            public async Task Any(BuildOrderReady @event)
            {
                await Handler(@event);
            }
    
            public async Task Any(PaySuccessReady @event)
            {
                await Handler(@event);
            }
        }
    }

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

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

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

    namespace Infrastructure.Commands
    {
        public interface ICommand : IReturn<CommandResult>
        {
            Guid CommandId { get; }
        }
    }
    namespace Infrastructure.Events
    {
        public interface IEvent : IReturnVoid
        {
            Guid EventId { get; } 
        }
    }

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

    namespace Application.Services.Config
    {
        public class AppHost : AppHostBase
        {
            public AppHost()
                : base("CQRS Demo", typeof(AppHost).Assembly) { }
    
            public override void Configure(Funq.Container container)
            {
                //SwaggerUI配置用于调试
                AddPlugin(new SwaggerFeature());
    
                //IOC配置
                ServiceLocatorConfig.Configura(container);
    
                //rabbitmq配置
                var mq = new RabbitMqServer(ConfigurationManager.AppSettings.Get("EventProcessorAddress"))
                {
                    AutoReconnect = true,
                    DisablePriorityQueues = true,
                    RetryCount = 0
                };
                container.Register<IMessageService>(c => mq);
                var mqServer = container.Resolve<IMessageService>();
    
                //注册eventHandler
                mq.RegisterHandler<BuildOrderReady>(ServiceController.ExecuteMessage, 1);
                mq.RegisterHandler<PaySuccessReady>(ServiceController.ExecuteMessage, 1);
    
                mqServer.Start();
            }
        }
    }

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

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

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

    image

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

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

    inq:还未被消费的消息。

    outq:处理完毕的消息。

    priorityq:优先队列。

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

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

    //注册eventHandler
    mq.RegisterHandler<BuildOrderReady>(ServiceController.ExecuteMessage, 1);
    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

  • 相关阅读:
    Spinnerd的功能和用法
    vagrant up ----失败 问题解决
    Yii2.0基础框架
    linux上nginx新建站点
    vagrant(二)配置文件vagrantfile详解 以及安装php、nginx、mysql
    vagrant(一)初识与安装
    cmd 使用gii的命令行用法
    mysql 使用shell时出现 ERROR 2006 (HY000): MySQL server has gone away 解决方法
    c++ virtual总结
    kartikgridGridView 合计,多选,导出excel,header修改 等方法集合!
  • 原文地址:https://www.cnblogs.com/idoudou/p/Domain-driven-design-Part2.html
Copyright © 2020-2023  润新知