DDD的落地,需要基础设施的大力支持

news2025/1/12 23:07:44

1. 概览

对于复杂业务,DDD 绝对是一把神器,由于它过于复杂,很多人望而却步。因为太过严谨,形成了很多设计模式、规范化流程,这些爆炸的信息已经成为 DDD 落地的重大阻力。

但,如果我们将这些规范化的流程封装到框架,仅把核心业务逻辑暴露给开发人员,又会是什么样子?

1.1. 背景

在尝试使用 DDD 处理复杂业务之后,就难以回到 CRUD 的世界。相对于 CRUD 来说,DDD 具备一套完整的理论基础,提供了一组业务模式和规范用以应对复杂的业务流程。但,由于其概念繁多,通常还过于抽象,存在一定的门槛;加上过于规范,业务流程被拆分多个组件,大大增加了理解成本,也加大了开发人员的代码量。

好在,由于规范所以产生了大量的最佳实践,日常开发中的众多业务场景均可完成抽象化、模板化甚至清单化,开发人员只需照“猫画虎”便可以完成DDD落地。而这些最佳实践,最好能够以“基础设施”的方式进行支持,降低入门门槛,提升开发效率。

1.2. 目标

  1. 将模板流程全部内置于框架,让业务开发人员将更多的精力聚焦于领域模型;
  2. 支持领域模型的 创建 和 更新 两大业务场景,只做接口定义,不编写流程代码;
  3. 核心流程需具备 参数校验、业务规则验证、Command 和 Context 转换,状态持久化、领域事件发布等通用能力;
  4. 支持自定义流程,对于个性化场景,可通过编码方式完成业务流程,并快速与 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,具体配置如下:

  1. domainClass。操作的领域对象,通常为一个聚合根;
  2. idClass。领域对象的主键类型;
  3. 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;
}

其核心逻辑包括:

  1. 绑定 Address 信息;
  2. 将要购买的 Product 转换为 OrderItem,并绑定到 Order 对象;
  3. 对订单状态进行初始化;
  4. 创建并保存领域事件;

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();
}

核心逻辑包括:

  1. 将 Command 对象转换为 Context 对象;
  2. 使用 LazyLoadFactory 创建 Proxy 对象,使其 Context 具有延迟加载能力;
  3. 使用 validateService 基于 Context 完成业务验证;
  4. 调用 Order.create 静态方法,完成 order 对象的创建;
  5. 将新建的 order 对象通过 Repository 保存到 DB;
  6. 基于 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);
}

核心操作包括:

  1. 创建 PayRecord 记录支付行为;
  2. 将订单状态变更为 已支付;
  3. 创建并保存领域事件;

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));
}

核心操作如下:

  1. 根据主键从 DB 中获取 聚合根Order 对象;
  2. 调用聚合根 order 的paySuccess 方法,执行业务操作;
  3. 调用 Repository 的 save 方法,将变更更新到 DB;
  4. 使用 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 接口,并将方法调用分发给不同的实现,核心拦截器包括:

  1. DefaultMethodInvokingMethodInterceptor。拦截对默认方法的调用,将请求转发给代理对象;
  2. 基于自定义实现的 MethodDispatcherInterceptor,将请求转发给自定义实现类;
  3. 基于自动创建 CreateServiceMethod 的 MethodDispatcherInterceptor,根据方法签名自动实现创建逻辑,并将请求转发给 CreateServiceMethod;
  4. 基于自动创建 UpdateServiceMethod 的 MethodDispatcherInterceptor,根据方法签名自动实现更新逻辑,并将请求转发给 UpdateServiceMethod;

3.2. 初始化流程

以下是整个框架的初始化流程:

 

image

通过 @EnableCommandService 注解开启 CommandService 支持后,将向 Spring 容器注册
CommandServiceBeanDefinitionRegistrar,由该组件完成 CommandService 的装配:

  1. InterfaceBeanDefinitionScanner 根据 basePackages 设置,自动对带有@CommandServiceDefinition的接口进行扫描;
  2. 扫描到带有@CommandServiceDefinition注解的接口后,将其封装为 CommandServiceProxyFactoryBean,并将其注册到 Spring 容器;
  3. Spring 实例化 CommandServiceProxyFactoryBean 生成对应的 CommandService 代理对象;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/10828.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1.4_28 Axure RP 9 for mac 高保真原型图 - 案例27【中继器 - 后台管理系统5】功能-弹窗修改数据

相关链接 目录Axure中文学习网AxureShopAxureShop-QA 案例目标1. 了解使用中继器&#xff0c;弹窗修改数据的实现方式 一、成品效果 Axure Cloud 案例27【中继器 - 后台管理系统5】功能-弹窗修改数据 版本更新一、修改功能   1.1 点击修改按钮&#xff0c;标记该条数据&am…

国产软件Bigemap与国产在线地图源<星图地球数据云>推动国内新GIS应用

自星图地球数据云(GEOVIS Earth Datacloud)图源成为国产基础软件Bigemap的在线地图数据服务平台之一以来&#xff0c;其日均地图瓦片请求调用量目前已经超过2亿。 “星图地球数据云"是中科星图(股票代码[688568])旗下子公司——星图地球倾力打造的在线时空数据云服务平台…

基于约束的装配设计【CadQuery】

本教程介绍在CadQuery中如何使用装配约束功能来构建逼真的模型&#xff0c;我们将组装一个由 20x20 V 型槽型材制成的门组件。 1、定义参数 我们希望从定义模型参数开始&#xff0c;以便以后可以轻松更改尺寸&#xff1a; import cadquery as cq# Parameters H 400 W 200…

2.8 高收藏率小红书笔记怎么写?试一试这7类方法吧【玩赚小红书】

1、教程攻略类 ​ ​ ​ 打开任何一类的美妆产品&#xff0c;最常见的就是各类妆容教程和变美攻略。就拿教程最多的眼妆来说吧&#xff0c;很多女孩子都觉得眼妆很难画好。 如果是碰到网上流行的网红眼影&#xff0c;比如什么猫眼妆、截断式眼影、桃花眼影等等。 【 高收藏秘…

社区团购小程序制作有什么优势_ 社区团购小程序的作用

打造属于自身的独立小程序拥有更高的自主性&#xff0c;特别是基于得店小程序的创新产品力&#xff0c;从设计上彰显品牌理念&#xff0c;到功能上进行扩展拓宽营销方式&#xff0c;都完全自我掌控&#xff0c; ● 更重要的是&#xff0c;相比于平台上各种复杂的机制&#xff0…

点击化学接头BCN-endo-PEG15-NH2,endo-BCN-十五聚乙二醇-胺

&#xff08;本品应密封避光&#xff0c;储存于阴凉&#xff0c;干燥&#xff0c;通风处&#xff0c;取用一定要干燥&#xff0c;避免频繁的溶解和冻干&#xff09; 【产品理化指标】&#xff1a; CAS&#xff1a;N/A 化学式&#xff1a;C43H80N2O17&#xff0c;分子量&#xf…

Vue3+TS+Vite 搭建组件库记录

使用pnpm 安装 npm install pnpm -g初始化package.json pnpm init新建配置文件 .npmrc shamefully-hoist true这里简单说下为什么要配置shamefully-hoist。 如果某些工具仅在根目录的node_modules时才有效&#xff0c;可以将其设置为true来提升那些不在根目录的node_modu…

防火墙安全策略

目录 一、包过滤技术 包过滤 安全策略 安全策略的原理 安全策略分类 二、防火墙的转发原理&#xff08;重点&#xff09; 首包流程 会话表 状态检测机制 会话在转发流程中的位置 多通道协议技术 ASPF 端口识别对多通道协议的支持 分片缓存 三、防火墙的安全策略配…

万应案例精选|跨壁垒、辅决策,万应低代码助力国网电力内部培训数字化架构升级

万应案例精选&#xff5c;跨壁垒、辅决策&#xff0c;万应低代码助力国网电力内部培训数字化架构升级一、项目背景 国网某省电力有限公司&#xff08;下称“国网电力”&#xff09;&#xff0c;是国家电网有限公司的全资子公司&#xff0c;现设20个职能部门&#xff0c;下设16…

智慧法院解决方案-最新全套文件

智慧法院解决方案-最新全套文件一、建设背景二、架构思路三、建设方案四、获取 - 智慧法院全套最新解决方案合集一、建设背景 智慧法院是指充分运用互联网、云计算、大数据、人工智能等技术&#xff0c;促进审判体系与审判能力现代化&#xff0c;实现人民法院 高度智能化的运行…

Java基础—普通阻塞队列

普通阻塞队列 除了刚介绍的两个队列&#xff0c;其他队列都是阻塞队列&#xff0c;都实现了接口BlockingQueue&#xff0c;在入队/出队时可能等待&#xff0c;主要方法有&#xff1a; 入队&#xff0c;如果队列满&#xff0c;等待直到队列有空间 void put(E e) throws Inter…

计算机毕业设计Python+Django的银行取号排队系统

项目介绍 随着信息技术和网络技术的飞速发展&#xff0c;人类已进入全新信息化时代&#xff0c;传统管理技术已无法高效&#xff0c;便捷地管理信息。为了迎合时代需求&#xff0c;优化管理效率&#xff0c;各种各样的管理系统应运而生&#xff0c;各行各业相继进入信息管理时…

MySQL之短时间提高性能的措施

文章目录前言短连接风暴慢查询QPS&#xff08;每秒查询数&#xff09;突增总结前言 我们在使用数据库的时候&#xff0c;总会有那么一段时间&#xff0c;数据库的压力会特别大。比如&#xff0c;用户的使用的高峰期&#xff0c;或者活动上线的时候等等。那么为了应对突然暴增的…

Child Tuning: 反向传播版的Dropout

这篇文章主要是对EMNLP2021上的论文Raise a Child in Large Language Model: Towards Effective and Generalizable Fine-tuning进行讲解。论文标题有些抽象&#xff0c;但是用作者的话来说&#xff0c;这篇论文的思想可以归结为两个词&#xff1a;Child Tuning 虽然这篇文章主…

0-1背包问题

在将什么是0-1背包问题前&#xff0c;先来看下面一道例题&#xff1a; 题意概要&#xff1a;有 n 个物品和一个容量为 W 的背包&#xff0c;每个物品有重量w i和价值v i两种属性&#xff0c;要求 选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容…

京东一小伙一年输出20篇专利,其实你也可以

前言&#xff1a; ☆ 本文属于技术总结类干货分享&#xff0c;是实战但又不同于实战&#xff0c;所以不能保证每位同学读后都可以立马自己也输出一篇合格的专利&#xff1b; ☆ 但通过本文的总结分享&#xff0c;已经帮身边同学半年内输出大于100篇专利&#xff0c;所以如果你细…

【字母识别】基于matlab BP神经网络英文字母识别【含Matlab源码 2226期】

⛄一、BP神经网络英文字母识别 1 典型前向网络——BP神经网络 前向网络是目前研究最多的网络形式之一, 它包含输入层、隐层以及输出层, 其中隐层可以为一层或者多层 , 其结构如图1所示. 图1 BP神经网络模型 BP神经网络误差反向传播学习算法的基本思想如下:向网络提供训练例子…

智慧公路筑基者!天翼云打造全栈能力新底座

11月9日-11日&#xff0c;在第十七届中国智能交通年会&#xff08;ITSAC 2022&#xff09;暨2022中国智能交通大会上&#xff0c;中国电信作为本届大会特别支持单位&#xff0c;于11月10日成功举办了主题为“构建智慧公路全栈能力新底座”的运营商赋能智慧交通创新论坛。 交通运…

windows 安装ElasticSearch(es)可视化工具

因项目需要&#xff0c;小编这里使用的是 npm版本 6.14.16 nodejs版本14.19.1 1、下载nodejs地址&#xff1a;https://nodejs.org/download/release/v14.19.1/ 版本需要可根据自己电脑进行选择 2、下载可视化项目包 下载地址&#xff1a;https://github.com/mobz/elasticse…

2020年聚合支付评级结果及如何开展评级工作经验分享

一年一度的收单外包服务机构评级工作即将启动&#xff0c;笔者认为479家聚合支付机构也在关心本机构要不要进行评级并希望了解聚合支付评级要求、评级对机构有何意义和影响、目前聚合支付评级情况及如何开展评级工作。为此&#xff0c;基于笔者最近作为两家聚合支付机构常年顾问…