暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【DDD】工具人之领域驱动设计实践

将咖啡转化为程序的工具人 2021-09-08
1869


上一期我们介绍了领域驱动设计【工具人】业务中台与领域驱动设计,今天我们接着看下在领域驱动设计实现过程中,经常会遇到的一些困惑。

一,领域对象状态一致性

DDD主要强调领域逻辑的高度内聚,和传统的MVC分层的过程式编写方式完全不同。传统的SOA模式中,服务是我们的设计原子,所以承载业务逻辑的主要是服务类对象,这些服务对象都会被设计成无状态对象,通常使用单例模式来构建服务实体。而业务中的状态变迁,常常被下沉至数据库逻辑中,来保证并发环境下的读写一致性。

举个例子:某理财产品拥有一定的销售额度,每当一个客户购买后,剩余额度都需要扣减。这样的逻辑简单的做法是在SQL上做原值更新。

    //伪代码
    public interface ProductService {
          boolean purchase(String productID,Double amount);
    }

    这里的amount是单次的购买量,而理财产品的剩余额度的扣减将在sql中完成。

      //伪代码
      update t_product set rest_amount = rest_amount - #{amount} 
      where product_id = #{productID}

      但是在DDD的模式下,聚合根则不再是简单的业务数据模型,而是领域逻辑的处理的载体。领域对象内部的状态变迁、数据变更等都将由聚合根统一处理。刚才那个例子在DDD的模式下会变成:

      聚合根:

        //伪代码
        public class Product{
            Double restAmount;
            public purchase(purchaseAmt){
                this.restAmount -= purchaseAmt;
            }
        }


        ApplicationService

          //伪代码
          @Override
          public void productPurchase(ProductPurchaseCmd cmd) {
          // validate
                  .....
          // find
                  Product product = productRepository.find(cmd.getProductId());
          // domain logic
                  product.purchase(cmd.getPurchaseAmt());
          // save
                  productRepository.update(product);
          }

          细心的同学会发现,这样的逻辑是不完整的,聚合根是个临时对象,将状态数据加载到聚合根内后变更,无法保证数据回写前,是否被别的线程或者进程篡改。所以,在修改状态数据时,必须加上乐观锁保证并发隔离。

          工具人常用的手段有两种:

          1,分布式锁

            //伪代码
            @Override
            public void productPurchase(ProductPurchaseCmd cmd) {
            // validate
            .....
                    locker.lock(cmd.getProductId())
                    try{
            // find
            Product product = productRepository.find(cmd.getProductId());
            // domain logic
            product.purchase(cmd.getPurchaseAmt());
            // save
            productRepository.update(product);
                    }
                    finnaly{
                     locker.unLock(cmd.getProductId());
                    }
            }

            2,数据版本号

            可以通过在数据上增加版本号信息,在读取领域对象的时候获取当前数据版本号,写入的时候比较版本号是否一致,若不一致,则说明数据被篡改了,此时,可以根据业务场景拒绝或者重试。

              //为代码
              update t_product set
                rest_amount = rest_amount - #{amount} 
                version = #{newVersion}
              where product_id = #{productID}
                and version = #{oldVersion}

              ‍‍二,跨领域业务

              在某些业务场景下,一次用户行为往往涉及到多个领域的业务,而在DDD的设计原则中,对于单个事件,只允许对一个聚合对象进行修改,由此产生的其他改变必须在单独的事务中完成。

              那么如果一个业务跨多个聚合对象该怎么办呢?

              我们还是以理财购买这个业务场景举例:当用户购买理财产品后,除了产品需要扣减产品剩余额度外,用户客群标签也需要发生变更,如:购买一定总额的理财产品后,用户等级会从白银用户变成黄金用户。

              此时,该业务除了修改了产品聚合根外,还需要调整用户聚合根。

              在这种场景下,领域事件会是一个不错的工具来解决这个问题。通过领域事件的方式可以达到各个组件之间的数据一致性,通过最终一致性取代事务一致性。

                 //伪代码
                @Override
                public void productPurchase(ProductPurchaseCmd cmd) {
                // validate
                .....
                // find
                Product product = productRepository.find(cmd.getProductId());
                // domain logic
                product.purchase(cmd.getPurchaseAmt());
                // save
                productRepository.update(product);
                        //发布领域事件        
                        domainEventPublisher.publish(new UserDomainEvent(cmd.getUserID(),cmd.getProductID(),cmd.getPurchaseAmt()));
                }       



                我们可以使用事件总线对领域事件进行解耦。

                  @Component
                  public class GuavaDomainEventPublisher implements DomainEventPublisher {


                  @Autowired
                  EventBus eventBus;


                  public void publish(Object event) {
                  eventBus.post(event);
                  }


                  }

                  三,查询职责分离

                  在传统模式下,常常会有这样的困惑。数据写模型往往与查询业务的集合差别明显,甚至不在一个纬度。导致数据模型设计越来越冗余,逐渐不可调和。DDD模式下,也有同样的困扰,在实现各式各样的查询功能时,往往会发现很难用领域模型来实现。

                  此时 CQRS 作为一种模式可以很好的解决以上的问题。

                  CQRS — Command Query Responsibility Segregation,顾名思义,就是将读写模型分离设计,作为两个独立的服务去实现。避免在设计上互相影响和干扰。

                  这是网上随处可见的一张示意图:


                  如此,我们可以为查询业务构建出查询侧的领域模型,并根据不同的查询场景,适配不同的查询服务。


                  其实,所有的架构设计都是可以在职场找到原型。

                  CQRS模式也不是什么新鲜的事物,很多公司常常就常常使用这种方式将工具人们的岗位和职责分离,责任和权利分离,贡献与待遇分离。

                  你加班补位,干了他的活,他邮件汇报,分了你的钱;

                  你用汗水做出贡献,他用口水吹出明天;

                  你是工具人,磨损生锈,无人心疼;他假装人工智能,空跑费电,人人追捧。

                  所以本周,又有位某部门的核心工具人不愿再忍,跑路了。

                  领导反复画饼,力透纸背,可惜无果。

                  不是饼画的不好,是纸太薄。








                  文章转载自将咖啡转化为程序的工具人,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论