如何一步一步用DDD设计一个电商网站(十二)—— 提交并生成订单

一、前言

之前的十一篇把用户购买商品并提交订单整个流程上的中间环节都过了一遍。现在来到了这最后一个环节,提交订单。

单从业务上看,这个动作的背后包含了多个业务操作,根据用户填写的订单信息生成订单、扣除使用的余额和积分、使用选择的礼券等等。

其中涉及到多个上下文的操作,包括新引入的订单上下文,那么如何同时与多个上下文进行数据的写入操作是本篇主要想讨论的问题。

 

二、解决数据一致性的方案

分布式系统中的多个子系统之间的同时写入问题,也就是所谓的数据一致性问题。

讲解决数据一致性方案的文章比较多,我就不赘述了,其中的根本是CAP理论,大家可自行百度/Google下。总结一下一般在分布式场景中无非就是两种方式来解决:2阶段提交的强一致性(选择CP)或者最终一致性(选择AP)。

2阶段提交大家都懂,是性能杀手,阻塞式的操作会导致整个系统的瓶颈提早到来。

最终一致性是非阻塞式的异步机制,通过消息体在多个系统内流转,并各自根据消息体来处理不同的业务,并且最终一致性有很多种形式来实现,这里暂不展开讨论。

 

三、回到DDD

在DDD中实现最终一致性需要引入一个之前一直没提到的概念:领域事件。

  问1:什么是领域事件?

  答:领域事件是领域的一部分,表示领域中所发生的事情。

  问2:它存在的作用是?

  答:①作为实现最终一致性的载体

    ②解耦

    ③通过事件让不同的上下文分散处理下游业务,减少对数据的反向获取。处理单元更小化。  

    ④对开闭原则(OCP:Open-Closed Principle)最好体现。

  问3:那么我们如何运用到DDD中?

  答:①哪怕是同一个上下文中的不同聚合也需要通过领域事件来进行同步。

    ②把领域事件设计成聚合,但是其中的大部分代表事件发生与过去的部分属性应该为只读。设计为聚合拥有了唯一标识这样便于跟踪事件、持久化和跨限界上下文交互。

    ③使用发布 —— 订阅的方式来处理事件,降低耦合。

    ④有时,有必要使用领域服务来注册事件订阅方。这样的动机可能和让应用服务来注册订阅方一样,但是此时我们可能有特定于领域的原因。

    ⑤领域事件的一个经验法则是这样的:领域事件中所包含的信息应该满足80%的消费方,虽然对于很多消费方来说,这些信息是多余的。

 

四、设计

根据上面的描述,设计了以下的几个对象进行实现领域事件的发布和订阅,如下图1:

图1

DomainEventBus是一个单例。

事件(继承自DomainEvent)的发布全部经由它来处理,分发失败的时候会抛出一个DistributeExceptionEvent的事件,由调用方决定后续的处理方式。

另外事件订阅者(继承自DomainEventSubscriber)也通过DomainEventBus来注册订阅。类型依赖图如下图2:

图2

 

五、实现

为了能够比较直观的表达当前这个提交订单业务操作的处理流程,我粗略画了个时序图,如下图3。

图3

这里的事件发布是订单上下文内的一个组件,是一个进程内操作。另外事件具体发布的目的地由不同的订阅者控制,暂时就列出了2个。

好了根据上面的时序图描述,下面贴出其中的核心代码:

1.事件订阅

            var types = Assembly.Load("Mall.Domain.Order.DomainEventSubscribers").GetTypes().Where(ent => !ent.IsGenericType && ent.GetInterface(typeof(IDomainEventSubscriber).FullName) != null).ToList();
            foreach (var type in types)
            {
                var subscriberInstance = Activator.CreateInstance(AppDomain.CurrentDomain, type.Assembly.FullName, type.FullName).Unwrap();
                var subscriber = (IDomainEventSubscriber)subscriberInstance;
                DomainEventBus.Instance().Subscribe(subscriber);
            }

2.和2个对订单创建事件的订阅者

    public class OrderCreatedSubscriberPaymentContext : DomainEventSubscriber<OrderCreated>
    {
        public override void HandleEvent(OrderCreated domainEvent)
        {
            //TODO anything

            throw new NotImplementedException();
        }
    }
    public class OrderCreatedSubscriberSellingPriceContext : DomainEventSubscriber<OrderCreated>
    {
        public override void HandleEvent(OrderCreated domainEvent)
        {
            //TODO anything
            throw new System.NotImplementedException();
        }
    }

3.事件发布

        public Result Create(OrderRequest orderRequest)
        {
            if (!string.IsNullOrWhiteSpace(orderRequest.CouponId))
            {
                var couponResult = DomainRegistry.SellingPriceService().IsCouponCanUse(orderRequest.CouponId, orderRequest.OrderTime);
                if (!couponResult.IsSuccess)
                    return Result.Fail(couponResult.Msg);
            }

            var orderId = DomainRegistry.OrderRepository().NextIdentity();
            var order = Domain.Order.Aggregate.Order.Create(orderId, orderRequest.UserId, orderRequest.Receiver,
                orderRequest.CountryId, orderRequest.CountryName, orderRequest.ProvinceId, orderRequest.ProvinceName,
                orderRequest.CityId, orderRequest.CityName, orderRequest.DistrictId, orderRequest.DistrictName,
                orderRequest.Address, orderRequest.Mobile, orderRequest.Phone, orderRequest.Email,
                orderRequest.PaymentMethodId, orderRequest.PaymentMethodName, orderRequest.ExpressId,
                orderRequest.ExpressName, orderRequest.Freight, orderRequest.CouponId, orderRequest.CouponName, orderRequest.CouponValue, orderRequest.OrderTime);

            foreach (var orderItemRequest in orderRequest.OrderItems)
            {
                order.AddOrderItem(orderItemRequest.ProductId, orderItemRequest.Quantity, orderItemRequest.UnitPrice, orderItemRequest.JoinedMultiProductsPromotionId, orderItemRequest.ProductName);
            }

            DomainRegistry.OrderRepository().Save(order);
            DomainEventBus.Instance().Publish(new OrderCreated(order.ID, order.UserId, order.Receiver));
            return Result.Success();
        }

注意其中标红的部分,暂时没有考虑出现异常的情况。另外这里的OrderCreated事件只是象征性的写一下,实际的事件需要哪些属性,只要贯彻好二八原则,设计一个满足80%场景下的直接可用,剩下的20%可以增加一些查询来满足实际业务需要。 

 

六、结语

如果说领域对象、应用层、仓储层等这些概念还和传统的三层架构傻傻分不清楚的话。那么领域事件应该是整个DDD中最容易理解的一部分概念,因为这一部分是独立于传统的三层架构之外的完全不同的部分,也是整个DDD设计中低耦合的关键。

本篇先进行了一个对领域事件最简单的实现,主要阐述了领域事件在整个项目设计过程中的作用和运用的方式。这是一个基础,在这个基础之上已经有很多成熟的解决方案可以让我们的系统做的更好。

下篇会主要讲关于异常的处理(上文中标红的那部分),数据一致性的保证等更好的提高系统可用性的部分。谢谢各位看官。

本文完整的源码地址:https://github.com/ZacharyFan/DDDDemo/tree/Demo12



原创文章,转载请注明本文链接: https://zacharyfan.com/archives/192.html

关于作者:张帆(Zachary,个人微信号:Zachary-ZF)。坚持用心打磨每一篇高质量原创。欢迎扫描二维码~

微信公众号

定期发表原创内容:架构设计丨分布式系统丨产品丨运营丨一些思考。

如果你是初级程序员,想提升但不知道如何下手。又或者做程序员多年,陷入了一些瓶颈想拓宽一下视野。欢迎关注我的公众号「跨界架构师」,回复「技术」,送你一份我长期收集和整理的思维导图。

如果你是运营,面对不断变化的市场束手无策。又或者想了解主流的运营策略,以丰富自己的“仓库”。欢迎关注我的公众号「跨界架构师」,回复「运营」,送你一份我长期收集和整理的思维导图。

Leave a Reply

发表评论

电子邮件地址不会被公开。 必填项已用*标注

ZacharyFan.com © 2019 | WordPress Theme: BlogGem by TwoPoints.