DDD落地需基础设施支持 -- 知识铺
1. 概览
对于复杂业务,DDD 绝对是一把神器,由于它过于复杂,很多人望而却步。因为太过严谨,形成了很多设计模式、规范化流程,这些爆炸的信息已经成为 DDD 落地的重大阻力。
但,如果我们将这些规范化的流程封装到框架,仅把核心业务逻辑暴露给开发人员,又会是什么样子?
1.1. 背景
在尝试使用 DDD 处理复杂业务之后,就难以回到 CRUD 的世界。相对于 CRUD 来说,DDD 具备一套完整的理论基础,提供了一组业务模式和规范用以应对复杂的业务流程。但,由于其概念繁多,通常还过于抽象,存在一定的门槛;加上过于规范,业务流程被拆分多个组件,大大增加了理解成本,也加大了开发人员的代码量。
好在,由于规范所以产生了大量的最佳实践,日常开发中的众多业务场景均可完成抽象化、模板化甚至清单化,开发人员只需照“猫画虎”便可以完成DDD落地。而这些最佳实践,最好能够以“基础设施”的方式进行支持,降低入门门槛,提升开发效率。
1.2. 目标
-
将模板流程全部内置于框架,让业务开发人员将更多的精力聚焦于领域模型;
-
支持领域模型的 创建 和 更新 两大业务场景,只做接口定义,不编写流程代码;
-
核心流程需具备 参数校验、业务规则验证、Command 和 Context 转换,状态持久化、领域事件发布等通用能力;
-
支持自定义流程,对于个性化场景,可通过编码方式完成业务流程,并快速与 CommandService 进行集成;
2. 快速入门
在设计上,CommandService 借鉴了 Spring Data 核心理念,在使用上也与 Spring Data 保存一致,以降低使用门槛。
2.1. 环境搭建
首先,在项目中引入 lego-starter,具体如下:
<dependency>
<groupId>com.geekhalo.lego</groupId>
<artifactId>lego-starter</artifactId>
<version>0.1.10-command_service-SNAPSHOT</version>
</dependency>
然后,依次引入 validation 和 spring data jpa 支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
在 application 文件中添加 Datasource 配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/lego
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
新增 SpringDataJpaConfiguration 配置类,完成对 spring data jpa 的配置,具体如下:
@Configuration
@EnableJpaRepositories(basePackages = {"com.geekhalo.lego.command"})
public class SpringDataJpaConfiguration {
}
新增 CommandServiceConfiguration 配置类,完成对 CommandService 的配置,具体如下:
@Configuration
@EnableCommandService(basePackages = "com.geekhalo.lego.command")
public class CommandServiceConfiguration {
}
其中,@EnableCommandService 开启 CommandService 自动扫描,扫描路径为:com.geekhalo.lego.command
新建 Order、OrderAddress、OrderItem、PayRecord 实体对象,并以 Order 为聚合根管理其他关联对象,Order 定义如下:
@Data
@Entity(name = "CommandOrder")
@Table(name = "command_order")
@Setter(AccessLevel.PRIVATE)
public class Order implements AggRoot<Long> {
@Transient
private final List<DomainEvent> events = Lists.newArrayList();
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id")
private Long userId;
@Column(name = "status")
@Enumerated(EnumType.STRING)
private OrderStatus status;
@Column(name = "price")
private int price;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "user_address_id")
private OrderAddress address;
@OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private List<OrderItem> items = Lists.newArrayList();
@OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "order_id")
private List<PayRecord> payRecords = Lists.newArrayList();
}
创建 OrderRepository,用于完成 Order 聚合根的持久化,具体如下:
@Repository("orderRepositoryForCommand")
public interface OrderRepository extends JpaRepository<Order, Long>,
CommandRepository<Order, Long> {
}
2.2. 定义 OrderCommandServiceProxy
@CommandServiceDefinition(
domainClass = Order.class,
idClass = Long.class,
repositoryClass = OrderRepository.class)
public interface OrderCommandServiceProxy{
}
定义 OrderCommandServiceProxy 接口,使用 @CommandServiceDefinition 将其声明为 CommandService,具体配置如下:
-
domainClass。操作的领域对象,通常为一个聚合根;
-
idClass。领域对象的主键类型;
-
repositoryClass。用于数据保存的仓库;
框架将自动创建 OrderCommandServiceProxy 的代理对象,并实现核心业务逻辑。
2.3. 创建订单(新建场景)
2.3.1. 核心业务操作
创建订单的核心逻辑由 Order 聚合根的静态 create 方法承载,具体如下:
public static Order create(CreateOrderContext contextProxy) {
Order order = new Order();
order.setUserId(contextProxy.getCommand().getUserId());
Address address = contextProxy.getAddress();
OrderAddress orderAddress = new OrderAddress();
orderAddress.setDetail(address.getDetail());
order.setAddress(orderAddress);
List<Product> products = contextProxy.getProducts();
Map<Long, Product> productMap = products.stream()
.collect(Collectors.toMap(Product::getId, Function.identity()));
List<ProductForBuy> productForBuys = contextProxy.getCommand().getProducts();
productForBuys.stream()
.map(productForBuy -> {
Product product = productMap.get(productForBuy.getProductId());
return OrderItem.create(product, productForBuy.getAmount());
}).forEach(orderItem -> order.addOrderItem(orderItem));
order.init();
OrderCreatedEvent event = new OrderCreatedEvent(order);
order.events.add(event);
return order;
}
其核心逻辑包括:
-
绑定 Address 信息;
-
将要购买的 Product 转换为 OrderItem,并绑定到 Order 对象;
-
对订单状态进行初始化;
-
创建并保存领域事件;
2.3.2. 手工编写业务流程
创建订单的业务流程位于 OrderCommandServiceImpl 的 create 方法,具体代码如下:
@Override
public Long create(CreateOrderCommand command) {
CreateOrderContext context = new CreateOrderContext(command);
CreateOrderContext contextProxy = this.lazyLoadProxyFactory.createProxyFor(context);
validateService.validate(contextProxy);
Order order = Order.create(contextProxy);
this.orderRepository.save(order);
order.consumeAndClearEvent(event -> eventPublisher.publishEvent(event));
return order.getId();
}
核心逻辑包括:
-
将 Command 对象转换为 Context 对象;
-
使用 LazyLoadFactory 创建 Proxy 对象,使其 Context 具有延迟加载能力;
-
使用 validateService 基于 Context 完成业务验证;
-
调用 Order.create 静态方法,完成 order 对象的创建;
-
将新建的 order 对象通过 Repository 保存到 DB;
-
基于 Spring Event 机制对外发布领域事件;
2.3.3. 自动创建 create 逻辑
核心业务操作随业务变化而变,而业务流程各个步骤基本不变,这些不变部分应该由框架来完成。
相比手工实现业务流程,使用 CommandService 只需在接口中增加 create 方法,将由 CommandService 框架为其生成代理实现,具体如下:
@CommandServiceDefinition(
domainClass = Order.class,
idClass = Long.class,
repositoryClass = OrderRepository.class)
public interface OrderCommandServiceProxy{
Long create(CreateOrderCommand command);
}
无需编写实现,只需定义接口
2.4. 支付成功(更新场景)
2.4.1. 核心业务操作
支付成功的核心业务操作位于 order 对象的 paySuccess 方法,具体如下:
public void paySuccess(PaySuccessCommand paySuccessCommand){
PayRecord payRecord = PayRecord.create(paySuccessCommand.getChanel(), paySuccessCommand.getPrice());
this.payRecords.add(payRecord);
this.setStatus(OrderStatus.PAID);
OrderPaySuccessEvent event = new OrderPaySuccessEvent(this);
this.events.add(event);
}
核心操作包括:
-
创建 PayRecord 记录支付行为;
-
将订单状态变更为 已支付;
-
创建并保存领域事件;
2.4.2. 手工编写业务流程
有了 paySuccess 核心业务操作,业务流程也变得非常简单,具体如下:
public void paySuccess(PaySuccessCommand command) {
Order order = this.orderRepository.findById(command.getOrderId())
.orElseThrow(() -> new AggNotFoundException(command.getOrderId()));
order.paySuccess(command);
this.orderRepository.save(order);
order.consumeAndClearEvent(event -> eventPublisher.publishEvent(event));
}
核心操作如下:
-
根据主键从 DB 中获取 聚合根Order 对象;
-
调用聚合根 order 的paySuccess 方法,执行业务操作;
-
调用 Repository 的 save 方法,将变更更新到 DB;
-
使用 Spring Event 机制,对外发布领域事件;
2.4.3. 自动创建 paySuccess 逻辑
聚合根更新操作的业务流程基本不变,这些不变部分应该由框架来完成。
相比手工实现业务流程,使用 CommandService 只需在接口中增加 paySuccess 方法,将由 CommandService 框架为其生成代理实现,具体如下:
@CommandServiceDefinition(
domainClass = Order.class,
idClass = Long.class,
repositoryClass = OrderRepository.class)
public interface OrderCommandServiceProxy{
Long create(CreateOrderCommand command);
void paySuccess(PaySuccessCommand command);
}
2.5. 取消订单(自定义业务逻辑)
如果业务逻辑并不是简单的创建和更新,而是更为复杂的定制化流程,这时便可以使用自定义逻辑进行扩展。
2.5.1. 自定义接口
首先,我们需要创建一个自定义接口,具体如下:
public interface CustomOrderCommandService {
void cancel(Long orderId);
}
2.5.2. 实现自定义接口
然后,按照业务需求,在 CustomOrderCommandServiceImpl 中实现业务逻辑。
@Service
public class CustomOrderCommandServiceImpl implements CustomOrderCommandService{
@Autowired
private OrderRepository orderRepository;
@Override
public void cancel(Long orderId) {
Order order = this.orderRepository.findById(orderId).orElseThrow(() -> new AggNotFoundException(orderId));
order.cancel();
this.orderRepository.save(order);
}
}
2.5.3. 与 OrderCommandServiceProxy 进行集成
最后,我们需要将自定义接口与 OrderCommandServiceProxy 进行集成。
只需让 OrderCommandServiceProxy 接口继承 CustomOrderCommandService 即可。
@CommandServiceDefinition(
domainClass = Order.class,
idClass = Long.class,
repositoryClass = OrderRepository.class)
public interface OrderCommandServiceProxy extends CustomOrderCommandService{
Long create(CreateOrderCommand command);
void paySuccess(PaySuccessCommand command);
}
在调用 cancel 方法时,proxy 会将请求转发给 CustomOrderCommandServiceImpl 的 cancel 方法。
3. 设计&扩展
3.1. Proxy 结构
为 CommandService 自动实现的 Proxy 结构如下:
image
Proxy 实现 自定义的CommandService 接口,并将方法调用分发给不同的实现,核心拦截器包括:
-
DefaultMethodInvokingMethodInterceptor。拦截对默认方法的调用,将请求转发给代理对象;
-
基于自定义实现的 MethodDispatcherInterceptor,将请求转发给自定义实现类;
-
基于自动创建 CreateServiceMethod 的 MethodDispatcherInterceptor,根据方法签名自动实现创建逻辑,并将请求转发给 CreateServiceMethod;
-
基于自动创建 UpdateServiceMethod 的 MethodDispatcherInterceptor,根据方法签名自动实现更新逻辑,并将请求转发给 UpdateServiceMethod;
3.2. 初始化流程
以下是整个框架的初始化流程:
image
通过 @EnableCommandService 注解开启 CommandService 支持后,将向 Spring 容器注册 CommandServiceBeanDefinitionRegistrar,由该组件完成 CommandService 的装配:
-
InterfaceBeanDefinitionScanner 根据 basePackages 设置,自动对带有@CommandServiceDefinition的接口进行扫描;
-
扫描到带有@CommandServiceDefinition注解的接口后,将其封装为 CommandServiceProxyFactoryBean,并将其注册到 Spring 容器;
-
Spring 实例化 CommandServiceProxyFactoryBean 生成对应的 CommandService 代理对象;
4. 项目信息
项目仓库地址:https://gitee.com/litao851025/lego
项目文档地址:https://gitee.com/litao851025/lego/wikis/support/Command
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/geek001/post/20240710/DDD%E8%90%BD%E5%9C%B0%E9%9C%80%E5%9F%BA%E7%A1%80%E8%AE%BE%E6%96%BD%E6%94%AF%E6%8C%81--%E7%9F%A5%E8%AF%86%E9%93%BA/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com