⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 geekhalo 「geekhalo」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

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

  1. “读”再复杂也是简单;
  2. “写”再简单也是复杂;

可见 Command 远比 Query 棘手的多,其中最关键的便是使用哪种模式来承载业务?

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

  1. 事务脚本。
  2. 领域模型。

1. 事务脚本 与 领域模型

❝

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

❞

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

  1. 事务脚本,门槛低上手快,适合简单的业务场景,比如资讯、博客等;
  2. 领域模型,门槛很高,适合处理复杂的业务场景,比如电商、银行、电信等;

大家最常听说也是最反感的便是:被别人称为 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. 区别

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

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

除此之外,DDD 还有很多的特点,比如:

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

2. 实战体验

❝

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

❞

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

  1. 创建场景。围绕电商下单流程进行说明。
  2. 更新场景。以电商订单改价流程为基础进行说明。

在日常开发中,物理删除场景用的非常少,甚至很多公司都明令禁止使用“delete”语句。通常使用 “逻辑删除” 替代,它可归属为标准的更新场景,在此暂不对比 物理删除场景。

2.1. 创建场景:下单

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

  1. 对商品库存进行校验,避免出现超卖的情况;
  2. 对商品库存进行锁定,如果支付成功则直接扣减锁定的库存;如果支付失败,则对锁定的库存进行归还;
  3. 为每一种购买的商品生成一个订单项(OrderItem),记录商品单价、购买数量、需付总价、应付金额等;
  4. 为每一笔下单生成一个订单(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<OrderItem> 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<OrderItem> 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<OrderItemDTO> 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;
}
}

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

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

2.2. 更新场景:订单改价

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

  1. 修改订单项价格(OrderItem),根据商品要支付金额对新价格按比例进行均摊;
  2. 修改订单价格(Order),修改订单的支付金额;

2.2.1. 基于事务脚本的订单改价

核心代码如下:

@Transactional  
public void changeOrderPrice(Long orderId, Long newPrice) {
// 1. 校验金额
if (newPrice <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}

// 校验订单有效性
Order order = orderMapper.getOrderById(orderId);
if (order == null) {
throw new IllegalArgumentException("订单不存在");
}

// 2. 对订单项价格进行均摊
allocateDiscount(order, order.getTotalPrice() - newPrice);

// 3. 修改订单价格
order.setTotalPrice(newPrice);
orderMapper.updateOrder(order);
}

public void allocateDiscount(Order order, Long discount) {
if (discount == 0){
return;
}

List<OrderItem> items = this.orderItemMapper.getByOrderId(order.getId());
Long totalAmount = order.getTotalPrice();
Long allocatedDiscount = 0L;

for (int i = 0; i < items.size(); i++) {
OrderItem item = items.get(i);
Long itemAmount = item.getSellingPrice();
if (i != items.size() - 1) {
// 按比例进行均摊
Long itemDiscount = itemAmount / totalAmount * discount;
// 重新设置金额
item.setPrice(item.getPrice() - itemDiscount);
// 记录累加金额
allocatedDiscount += itemDiscount;
}else {
// 分摊余下的优惠金额到最后一个订单
Long lastItemDiscount = discount - allocatedDiscount;
item.setPrice(item.getPrice() - lastItemDiscount);
}
// 更新数据库
this.orderItemMapper.update(item);
}
}

和所描述的操作流程完全一致,成功使用“编程语言”完成了对需求的翻译。

2.2.2. 基于 DDD 的订单改价

核心代码如下:

@Transactional  
public void changeOrderPrice(Long orderId, Long newPrice) {
// 1. 校验金额
if (newPrice <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}

// 2. 获取订单聚合根
Optional<Order> orderOpt = this.orderRepository.getById(orderId);

Order order = orderOpt.orElseThrow(() -> new IllegalArgumentException("订单不存在"));

// 3. 修改价格
order.changePrice(newPrice);

// 4. 保存 Order 聚合
this.orderRepository.save(order);

}

// Order 聚合根内方法
public void changePrice(Long newPrice) {
if (newPrice <= 0) {
throw new IllegalArgumentException("金额必须大于0");
}

long discount = getTotalPrice() - newPrice;
if (discount == 0){
return;
}
// Item 均摊折扣
discountForItem(discount);
// Order 折扣
discountForOrder(discount);
}

// Item 均摊
private void discountForItem(long discount) {
Long totalAmount = getTotalPrice();
Long allocatedDiscount = 0L;

for (int i = 0; i < getOrderItems().size(); i++) {
OrderItem item = getOrderItems().get(i);
Long itemAmount = item.getSellingPrice();
if (i != getOrderItems().size() - 1) {
// 按比例进行均摊
Long itemDiscount = itemAmount / totalAmount * discount;
// 重新设置金额
item.setPrice(item.getPrice() - itemDiscount);
// 记录累加金额
allocatedDiscount += itemDiscount;
}else {
// 分摊余下的优惠金额到最后一个订单
Long lastItemDiscount = discount - allocatedDiscount;
item.setPrice(item.getPrice() - lastItemDiscount);
}
}
}
// Order 折扣
private void discountForOrder(long discount) {
Long newTotalPrice = getTotalPrice() - discount;
setTotalPrice(newTotalPrice);
}

和生单流程一样:

  1. 应用服务中的 changeOrderPrice 方法内容非常简单,标准的模版代码,变化可能性非常小,需要对其进行封装;
  2. 核心逻辑全部在 Order 聚合根中,通过 changePrice 方法 完成改价逻辑,业务逻辑非常集中,形成拥有属性和行为的“富对象”;
  3. 数据操作与逻辑解耦,最后一步操作 orderRepository#save 方法 完成内存对象向DB数据的同步,其他部分均不涉及基础设施;

2.3. 对比

看过这两种风格代码有什么感觉?你可能会说代码也没少些什么,只是组织方式发生了变化。

确实是,只是组织方式发生变化,代码一行都没少。这是这点变化,带来了革命的创新。

来看个新的场景:业务改价过于随意,产品想增加一个环节:填入改价金额后,先把每个订单项的均摊价格展示出来,确认无误后在提交改价请求。

在不同的模式下,又该怎么解呢?

  1. 事务脚本模型下,大概率会 copy 一个新的 changePrice,并在其基础上进行修改。这将产生代码的冗余,比如原来 changePrice 方法存在bug,在修复时你需要修改多处,但往往只会想起一处;
  2. DDD模型下,你只需获取 Order 聚合,然后调用 changePrice 方法,把均摊结果进行返回,便可实现想要的结果;

3. 小节

事务脚本 和 领域模型 是承载业务的不同模式,都有各自适用的场景,需要根据自己的需求进行选择。

事务脚本:流程 + 数据,在操作流程中对数据进行操作;

领域模型:编排 + 模型 + 数据,基于模型能力进行编排,以完成业务操作;操作结果暂存于对象中,最后将其同步到数据库;

DDD 灵活性还体现在:

  1. 流程组合,添加一个新功能,一次性完成生单和改价操作;
  2. 封装不变,创建和更新主流程基本一致,可以对其进行封装,以统一操作;
  3. 应用模式,逻辑由聚合对象承接,各种模式都可以拿来使用,比如设计模式、架构模式、领域模式等;
  4. 局部标准化,基于 DDD 战术体系,构建标准的编程模型;
文章目录
  1. 1. 1. 事务脚本 与 领域模型
    1. 1.1. 1.1. 事务脚本
    2. 1.2. 1.2. 领域模型
    3. 1.3. 1.3. 区别
  2. 2. 2. 实战体验
    1. 2.1. 2.1. 创建场景:下单
      1. 2.1.1. 2.1.1. 基于事务脚本的下单
      2. 2.1.2. 2.1.2. 基于 DDD 的下单
    2. 2.2. 2.2. 更新场景:订单改价
      1. 2.2.1. 2.2.1. 基于事务脚本的订单改价
      2. 2.2.2. 2.2.2. 基于 DDD 的订单改价
    3. 2.3. 2.3. 对比
  3. 3. 3. 小节