DDD 对决:事务脚本 vs. 领域模型,哪个才是业务优化的终极方案?

2023年 8月 29日 73.7k 0

在 CQRS 架构篇提到,由于 Command 和 Query 内部驱动力完全不同,需要在架构层就进行分离,但其中有个一个原则极为重要:

  • “读”再复杂也是简单;
  • “写”再简单也是复杂;
  • 可见 Command 远比 Query 棘手的多,其中最关键的便是使用哪种模式来承载业务?

    最常见的业务承载模式有:

  • 事务脚本。
  • 领域模型。
  • 1. 事务脚本 与 领域模型

    事务脚本 和 领域模型 都是承载业务的不同模型,都有其适合的场景,没有绝对的对和错。核心的决策依据只有一个:选择最合适的业务场景即可。

    简单且直观的对两者进行区分:

  • 事务脚本,门槛低上手快,适合简单的业务场景,比如资讯、博客等;
  • 领域模型,门槛很高,适合处理复杂的业务场景,比如电商、银行、电信等;
  • 大家最常听说也是最反感的便是:被别人称为 CRUD boy,更多时候说的便是 事务脚本。

    1.1. 事务脚本

    事务脚本(Transaction Script)是一种应用程序架构模式,主要用于处理简单的业务场景。它将业务逻辑和数据库访问紧密耦合在一起,以便实现对数据的操作。

    事务脚本,将整个业务逻辑封装在一个事务中,借助数据库事务来满足业务操作的 ACID 特性。通过将逻辑和事务封装在一起,从而简化应用程序的处理和开发。

    下图是基于事务脚本的生单流程:

    图片图片

    简单描述就是:将“脚本”(SQL)进行打包,然后放在一个“事务”中运行。这也就是“事务脚本”命名的由来。

    接下来,看一个订单改价流程:

    图片图片

    和生单流程基本一致,在此不做过多介绍。

    1.2. 领域模型

    领域驱动设计(Domain-Driven Design,DDD)是应对复杂业务场景的利器,它是对业务领域中的关键概念和业务规则的抽象。领域模型是一个对象模型,它主要描述各领域对象之间的关系和行为。

    和事务脚本不同,领域模型使用对象来承载业务逻辑,领域模型的设计基于业务领域知识,强调领域专家的参与,以提高软件系统的质量和开发效率。

    下图是基于领域模型的生单流程:

    图片图片

    简单描述就是:核心业务逻辑全部由对象实现(addItems方法),数据库仅做数据存储。

    接下来,看下基于DDD的订单改价流程:

    图片图片

    和生单流程基本一致,核心逻辑由 Order 的 modify price 实现。

    相比之下,领域模型就复杂太多,它由多个实体 (Entity)、值对象 (Value Object)、聚合 (Aggregate)、领域服务 (Domain Service)、工厂 (Factory) 等组成,它们共同构成了领域对象模型。在模型中,实体和值对象表示业务中的实际对象,聚合是由多个高内聚实体和值对象形成的组合提,领域服务表示不属于任何一个实体或值对象的操作,工厂则用于创建复杂的对象,比如实体和值对象等。

    1.3. 区别

    两者都是承载业务逻辑的架构,但区别巨大:

  • 事务脚本是以流程为中心的设计方法,在数据库层面执行指令,简化数据处理的过程;DDD 是以领域对象为中心的设计方法,旨在更好地理解和解决业务问题。
  • 事务脚本以技术和流程为重点,以技术为中心,以代码实现为核心,关注数据处理问题;DDD 则强调模型驱动开发,以业务为中心,以领域模型为核心,关注业务逻辑,并以此为基础进行技术实现。
  • 事务脚本很容易造成代码的累积,难以维护;DDD 能够帮助开发人员找到领域的本质(深层模型),并以此为核心,从而形成统一的、易于维护的架构。
  • 除此之外,DDD 还有很多的特点,比如:

  • 标准化。DDD 由一组严谨的规范组成,有完整的理论基础,可以实现落地过程的标准化;
  • 设计模型。大家在日常工作中很少使用设计模型的根因在于:缺乏应用场景。当你处于“过程式”的开发模式下,只能产出面条代码;只有面对“面对对象”场景,才能落地设计模式,提升抽象能力;
  • 降维打击。DDD 是从业务需求出发,将业务概念转化为对象模型,最后通过技术进行落地。这本身就是一种自上而下的设计方式,聚焦于业务,解决真实问题;
  • 2. 实战体验

    对于程序员来说,文字显得不够直观,在此我们通过代码来体验下两者的不同。

    为了更好的体现两者的区别,将会从两个场景进行对比:

  • 创建场景。围绕电商下单流程进行说明。
  • 更新场景。以电商订单改价流程为基础进行说明。
  • 在日常开发中,物理删除场景用的非常少,甚至很多公司都明令禁止使用“delete”语句。通常使用 “逻辑删除” 替代,它可归属为标准的更新场景,在此暂不对比 物理删除场景。

    2.1. 创建场景:下单

    在电商中,一个标准的下单需求主要包括:

  • 对商品库存进行校验,避免出现超卖的情况;
  • 对商品库存进行锁定,如果支付成功则直接扣减锁定的库存;如果支付失败,则对锁定的库存进行归还;
  • 为每一种购买的商品生成一个订单项(OrderItem),记录商品单价、购买数量、需付总价、应付金额等;
  • 为每一笔下单生成一个订单(Order),记录用户、地址、支付金额、订单状态等;
  • 2.1.1. 基于事务脚本的下单

    核心代码如下:

    @Transactional
    public void createOrder(CreateOrderCommand createOrderCommand) {
        // 1. 库存校验
        for (OrderItemDTO itemDTO : createOrderCommand.getItems()) {
            Integer stock = inventoryMapper.getStock(itemDTO.getProductId());
            if (stock < itemDTO.getQuantity()) {
                throw new IllegalStateException("库存不足");
            }
        }
        // 2. 锁定库存
        for (OrderItemDTO itemDTO : createOrderCommand.getItems()) {
            inventoryMapper.lockStock(itemDTO.getProductId(), itemDTO.getQuantity());
        }
    
        // 3. 生成订单项
        List items = createOrderCommand.getItems().stream()
                .map(OrderItem::create)
                .collect(Collectors.toList());
        orderItemMapper.createOrderItems(items);
    
        // 4. 生成订单
        Long totalPrice = items.stream()
                .mapToLong(OrderItem::getPrice)
                .sum();
    
        Order order = new Order(createOrderCommand.getUserId(),  totalPrice, OrderStatus.CREATED);
        orderMapper.createOrder(order);
    
    }

    事务脚本与需求所需操作流程完全一致,简单来说就是使用“编程语言”对需求进行了翻译。

    2.1.2. 基于 DDD 的下单

    核心代码如下:

    public void createOrder(CreateOrderCommand createOrderCommand) {
        // 1. 检查库存,如果足够则进行锁定;如果不够,则抛出异常
        this.inventoryService.checkIsEnoughAndLock(createOrderCommand.getItems());
    
        // 2. 创建 Order 聚合,此处使用静态工厂创建复杂的 Order 对象
        Order order = Order.create(createOrderCommand);
    
        // 3. 保存 Order 聚合, @Transactional 在OrderRepository上
        this.orderRepository.save(order);
    }
    
    public class Order {
        private Long id;
        private Long userId;
        private Long totalSellingPrice = 0L;
        private Long totalPrice = 0L;
        private OrderStatus status;
        private List orderItems = new ArrayList();
    
        // 避免外部调用
        private Order(Long userId) {
            this.userId = userId;
        }
    
        // 静态工厂,封装复杂的 Order 创建逻辑,并保障创建的 Order 对象是有效的
        public static Order create(CreateOrderCommand createOrderCommand) {
            Order order = new Order(createOrderCommand.getUserId());
            order.addItems(createOrderCommand.getItems());
            order.init();
            return order;
        }
    
        // 添加 OrderItem,并计算总金额
        private void addItems(List items) {
            if (!CollectionUtils.isEmpty(items)){
                items.forEach(item ->{
                    OrderItem orderItem = OrderItem.create(item);
                    this.orderItems.add(orderItem);
                    this.totalPrice += item.getPrice();
                });
            }
            this.totalPrice = totalSellingPrice;
        }
    
        // 设置状态完成对象的初始化
        private void init() {
            this.status = OrderStatus.CREATED;
        }
    }

    和事务脚本相比,由以下几点不同:

  • 应用服务中的 createOrder 方法内容非常简单,可以看做是模版代码,变化的可能性非常小,可以对其进行进一步的封装;
  • 核心逻辑全部在 Order 聚合根中,通过静态方法 create 完成 Order 对象的创建,业务逻辑非常集中,形成了拥有属性和行为的“富对象”;
  • 数据操作与逻辑解耦,最后一步操作 orderRepository#save 方法 完成内存对象向DB数据的同步,其他部分均不涉及基础设施;
  • 2.2. 更新场景:订单改价

    在电商中,订单改价主要包括:

  • 修改订单项价格(OrderItem),根据商品要支付金额对新价格按比例进行均摊;
  • 修改订单价格(Order),修改订单的支付金额;
  • 2.2.1. 基于事务脚本的订单改价

    核心代码如下:

    @Transactional
    public void changeOrderPrice(Long orderId, Long newPrice) {
    // 1. 校验金额
    if (newPrice

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论