DDD学习

概述

学习一下DDD
参考文章:
如何做DDD领域驱动设计
领域驱动设计DDD|从入门到代码实践

什么是DDD

DDD是领域驱动设计(Domain Driven Design)的缩写,是一种软件开发方法论。它强调将业务领域划分为多个紧凑的、自包含的领域,并通过强调领域模型的重要性来建立一个通用的语言和理解。

DDD鼓励开发者们更多地关注业务本身,而不是纯粹的技术实现。在DDD中,开发者将应用程序分为多个相互依赖但相对独立的领域,并通过领域模型来描述这些领域的内部结构和行为。通过使用聚合、实体、值对象、服务等概念,开发者可以更好地表示领域的真实世界,并将其映射到软件系统中。

DDD还提供了许多有用的设计模式,例如事件溯源、领域事件、限界上下文等,以帮助开发者更好地组织代码、管理复杂性,并提高系统的可维护性和可扩展性。同时,DDD还强调团队协作和沟通的重要性,通过建立通用的语言和理解来消除偏差和误解,从而推动项目的成功。

总之,DDD是一种注重业务本质、强调领域建模、提高设计品质和代码质量的软件开发方法论,其目标是构建高质量、易维护、适应变化的软件系统。

DDD的一些概念

原文:如何做DDD领域驱动设计
构建一个虚拟业务场景:小型电商网站,商家可出售商品,买家可选择、购买商品,购买后把款项结算给卖家。
在这里插入图片描述

战略设计

战略设计指的是对整个领域进行分析和规划,确定领域中的概念、业务规则和领域边界等基础性问题。在战略设计中,需要对领域进行全面的了解和分析,探究业务的规则和本质,并且需要考虑到领域的未来发展趋势和可能的变化。领域、子域和限界上下文属于战略设计的范畴。

领域

是指一个具有独立性和自治性的问题空间。听着比较抽象,实际上没那么复杂。就拿我们的虚拟场景来说,领域就是“电商”。对于【领域】来说,最重要的是两个概念:范围 + 知识体系。“范围”告诉我们,领域是有边界的。“知识体系”告诉我们,领域内的概念和规则是有专业性的且相互关联的。

子域

将领域划分,变成一块块更小的“领域”。每个“小领域”也必须具有“范围+知识体系”的特征。子域划分使用了分而治之的经典思想。在我们的虚拟场景中就会包括“商品域”、“支付域”、“结算域”等等

限界上下文

是针对子域更小的划分,其中包含了多个具体的领域模型。限界上下文的作用是将业务紧密关联的领域模型放在一起,并且提供统一的语境(各种概念在限界上下文中有统一的含义)。例如虚拟场景中,一个商品上单时我们将用户输入的卖价称之为原价,售卖时则将用户不使用任何优惠活动的价格称之为原价。“限界上下文”对微服务拆分有重要的指导意义。

领域模型

找到的和业务相关的各种模型。例如各种单据模型、角色模型、商品模型、权限模型等等。其中,很多模型之间有非常紧密的关系。
这四个概念我们是从大到小来说的,但在做战略设计的时候,其实是反过来的,这个我们后面就会看到。

战术设计

战术设计则是在战略设计的基础上,对领域中的具体问题进行具体的解决方案设计。战术设计关注的是领域中的具体情境和场景,需要针对具体的问题进行具体的分析和设计,以满足业务需求。实体、值对象、聚合、工厂、资源库、领域服务和领域事件就属于战术设计的范畴。
【领域服务的识别】和【代码层次的划分】

实体

指在业务领域中具有生命周期和业务行为并能产生变化的对象,如订单、客户等。

值对象

是指在业务领域中用来描述某些特定属性或属性组合的对象,通常是不可变的,如地址、手机号等。

聚合与聚合根

是用来组织“实体”和“值对象”的一种模式。聚合定义了一个业务逻辑上的整体,其中包含多个“实体”和“值对象”。聚合根是聚合中的一个实体,负责维护聚合内的完整性和一致性。

工厂

工厂是一种重要的设计模式。将创建复杂对象和聚合的职责分配给一个单独的对象,该对象本身并不承担领域模型中的职责,但是依然是领域设计的一部分。工厂应该提供一个创建对象的接口,该接口封装了所有创建对象的复杂操作过程,同时,它并不需要客户去引用那个实际被创建的对象。对于聚合来说,我们应该一次性地创建整个聚合,并且确保它的不变条件得到满足。

资源库

资源库(Repository)是一种模式,用于封装数据访问逻辑,提供对数据的持久化和查询。它旨在将数据访问细节与领域模型分离,使领域模型更加独立和可测试。资源库提供了一种统一的接口,使得领域模型可以与不同的数据存储方式(如关系数据库、文档数据库、内存数据库等)进行交互,同时也提供了一些查询操作,以便在领域层中进行数据查询。如果我们使用MyBatis的话,Mapper就是对资源库的一种实现。

领域服务

是指处理业务逻辑的函数,没有“实体”或“值对象”,它负责处理需要多个对象协作才能完成的复杂业务场景。

领域事件

是指在业务领域中发生的某些重要事件,它们会对业务流程产生影响。同时,事件还将通知所有对此事件感兴趣的相关方,比如其他领域、系统或模块。

领域建模

领域驱动设计的核心在于领域建模,架构师的水平高低在很大程度上也体现在领域建模水平上。
领域建模的主要目的是捕捉业务知识,形成统一语言,沉淀领域模型。好的领域建模就意味着对业务要有深刻的理解,能够洞察问题本质。领域建模的产出物一般有以下内容:

  1. 领域模型:包含领域对象、属性、关系、行为、边界范围等各个方面,用于描述业务的本质,这也是最重要的产出物。
  2. 用例图:用于明确系统的功能。
  3. 数据模型:描述系统的数据结构和关系,包括实体关系模型、关系数据库模型等。
  4. 状态图:用于描述系统各个状态及其转移条件。
  5. 活动图:用于描述系统流程中的各个活动及其关系。
  6. 序列图:描述系统中各个对象之间的交互过程和消息传递序列。
  7. 架构模型:包含系统的物理和逻辑结构,包括组件、模块、接口等。

下面介绍两种常见的领域建模方法。

事件风暴建模

领域驱动设计DDD|从入门到代码实践
个人理解:
就是一伙人讨论,围绕各业务的流程+业务规则来讨论,最终会形成各业务的完整流程图。

四色建模法

领域驱动设计DDD|从入门到代码实践
暂时还没看懂(适合现金流或者有明确Kpi指标的系统)

DDD的springboot工程目录结构

在使用DDD的情况下,一个Spring Boot项目的目录结构应该如下:

├── src
│   ├── main
│   │   ├── java
│   │   │   ├── com.example.myapp
│   │   │   │   ├── config               // 存放应用程序的配置类
│   │   │   │   │   └── AppConfig.java   // 应用程序配置类,用于配置应用程序的各种组件
│   │   │   │   ├── domain               // 领域层,存放领域模型相关的类
│   │   │   │   │   ├── model            // 存放领域模型
│   │   │   │   │   │   ├── AggregateRoot.java  // 聚合根基类,表示领域模型中的聚合根
│   │   │   │   │   │   ├── Entity.java         // 实体基类,表示领域模型中的实体
│   │   │   │   │   │   ├── ValueObject.java    // 值对象基类,表示领域模型中的值对象
│   │   │   │   │   │   └── ...           // 其他领域模型类
│   │   │   │   │   ├── event            // 事件实体及处理逻辑
│   │   │   │   │   ├── repository       // 存放领域模型的仓储接口
│   │   │   │   │   │   ├── UserRepository.java  // 用户仓储接口,用于定义对用户领域模型的持久化操作
│   │   │   │   │   │   └── ...           // 其他仓储接口
│   │   │   │   │   ├── service          // 存放领域服务接口
│   │   │   │   │   │   ├── UserService.java  // 用户服务接口,定义用户领域相关的业务逻辑操作
│   │   │   │   │   │   └── ...           // 其他服务接口
│   │   │   │   │   ├── vo               // 值对象
│   │   │   │   │   └── ...              // 其他领域模型相关类
│   │   │   │   ├── infrastructure      // 基础设施层,存放基础设施相关的类
│   │   │   │   │   ├── persistence     // 存放数据访问层实现
│   │   │   │   │   │   ├── UserRepositoryImpl.java  // 用户仓储实现类,实现对用户领域模型的持久化操作
│   │   │   │   │   │   └── ...         // 其他数据访问层实现类
│   │   │   │   │   └── messaging       // 存放消息处理相关类
│   │   │   │   │       ├── MessagePublisher.java  // 消息发布者,负责将消息发布到消息队列
│   │   │   │   │       └── ...         // 其他消息处理相关类
│   │   │   │   │   └── cache           // 缓存实现
│   │   │   │   │   └── mq              // 消息实现
│   │   │   │   │   └── rpc             // 外部调用实现
│   │   │   │   ├── interface           // 接口层
│   │   │   │   │   ├── controller      // http接口
│   │   │   │   │   ├── convertor       // 出入参数转换器
│   │   │   │   │   ├── dto             // 数据传输模型
│   │   │   │   │   ├── facade          // RPC接口
│   │   │   │   └── MyApplication.java  // Spring Boot 应用程序入口类,包含 main 方法
│   │   │   └── ...                     // 其他包和类
│   │   ├── resources                   // 存放资源文件
│   │   │   ├── application.yml         // 应用程序配置文件,包含应用程序的配置信息
│   │   │   └── ...                     // 其他资源文件
│   │   └── ...                         // 其他主目录下的子目录和文件
│   └── test                            // 测试代码目录
│       ├── java                        // 测试用例代码目录
│       │   └── ...                     // 其他测试用例代码
│       └── resources                   // 测试资源文件目录
│           └── ...                     // 其他测试资源文件
└── ...                                 // 其他目录和文件

在这个结构中,主要包含了以下几个部分:

  • config:用于存放应用程序的配置类;
  • domain:用于存放领域层相关的代码,包括实体、聚合根、值对象等;
  • infrastructure:用于存放基础设施层相关的代码,包括数据访问、消息发送等;
  • MyApplication:应用程序的入口文件。

此外,在domaininfrastructure目录下还会有对应的单元测试和集成测试代码。

另外值得注意的是,在DDD中,我们通常会将代码组织成基于业务领域(bounded context)的模块,因此上述目录结构并不是固定的,具体的组织方式应该根据应用程序的需求进行调整。

DDD与常用的Controller、Service、Dao分层有什么不同

常用的 Controller、Service、Dao 分层是一种基于三层架构的分层模式,主要关注技术实现方面的分层,以提高代码的可读性、可维护性和可扩展性等目标。在这种分层模式下,通常会将应用程序按照控制层、服务层、数据访问层进行划分,每个层次都有特殊的职责和工作。

而 DDD 则是一种面向领域的设计方法,它强调将业务逻辑与底层技术实现分离,并通过领域模型来描述和理解业务和系统的本质。DDD 认为应用程序开发的核心是领域模型的设计和实现,因此需要将软件设计和实现过程中的精力放在对领域的深入了解上,同时尽可能地消除技术实现带来的干扰。

DDD 与常用的 Controller、Service、Dao 分层的不同之处在于 DDD 更加注重领域的建模和设计,而常用的分层则更加注重技术实现的分层。在 DDD 中,核心思想是将具体的业务逻辑和领域知识进行建模,通过分析业务场景和业务需求,抽象出领域模型和相应的业务规则。这样可以将业务逻辑嵌入到领域模型中,从而更加清晰地表达业务需求和业务流程,同时也使得系统更加容易扩展和维护。

Controller,Service,Dao分层相比于DDD有什么优缺点

常用的 Controller、Service、Dao 分层是一种基于三层架构思想的分层设计模式,具体如下:

  1. Controller 层:主要负责处理用户请求,并将请求转发给 Service 层进行处理。通常包含 URL 映射、参数校验、控制事务、数据格式转换等功能。

  2. Service 层:主要负责业务逻辑的处理和实现,接收来自 Controller 层的请求,并根据业务需求调用 Dao 层提供的数据访问接口进行 CRUD 操作。同时,Service 层也负责对业务逻辑进行封装,提供接口给外部系统或内部其他系统使用。

  3. Dao 层:主要负责数据访问,通过与数据库连接,实现对数据的增删改查等操作,并提供与业务逻辑相对应的接口给 Service 层使用。

相比于 DDD 的目录结构,常用的 Controller、Service、Dao 分层更侧重于技术实现方面,而不关注业务场景中的领域划分和领域专家参与。其优缺点如下:

优点:

  1. 设计简单易懂:常用的 Controller、Service、Dao 分层虽然简单,但能够清晰地把职责分离,代码易于理解和维护。

  2. 灵活性强:常用的 Controller、Service、Dao 分层基于技术实现,因此在开发过程中可以灵活应对变化,快速迭代产品需求。

  3. 适用范围广:常用的 Controller、Service、Dao 分层可以适用于大部分业务场景,通用性较强。

缺点:

  1. 难以应对复杂业务场景:常用的 Controller、Service、Dao 分层没有强调业务逻辑建模,无法支撑大规模高复杂度业务的开发。

  2. 分层划分不够精细:常用的 Controller、Service、Dao 分层划分较为简单,无法有效地解决复杂业务场景下的问题。例如,在传统分层结构下,往往会存在因过多重复或耦合导致的代码臃肿和可维护性差等问题。

  3. 开发过程中需要考虑数据和业务之间的映射关系:常用的 Controller、Service、Dao 分层虽然创建了三个分层,但开发过程中仍需要考虑数据和业务之间的映射关系,增加了开发难度。

综上所述,虽然常用的 Controller、Service、Dao 分层与 DDD 的目录结构差异较大,但在不同场景下都有各自适用的情况。

具体场景对比

假设我们要开发一个电商平台,需要实现一个下单功能。我们来比较一下常用的分层与 DDD 的实现方式。

常用的分层实现

在常用的分层实现中,我们可能会按照控制层、服务层、数据访问层进行划分,大概的流程如下:

  1. 控制器接收到用户的请求,验证参数合法性。
  2. 服务层负责根据业务逻辑进行相应的处理,例如查询商品信息、检测库存情况等等。
  3. DAO层通过数据库操作,对库存情况进行更新。
  4. 服务层将订单信息写入数据库中,并返回成功状态给控制器。
  5. 控制器最终向用户返回成功信息或者错误信息。

这种实现方式主要关注技术实现上的分层,使用了标准的 CRUD 操作(Create、Retrieve、Update、Delete),从而提高了系统的可读性、可维护性和可扩展性。但是,这种实现方式通常比较注重系统的技术方面,而没有很好地考虑到业务逻辑和领域模型。

具体代码:
下面是一个使用常规分层实现编写的订单服务:

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private ProductService productService;

    @Autowired
    private OrderService orderService;

    @PostMapping("/add")
    public Result addOrder(@RequestBody OrderRequest request) {
        // 验证参数合法性
        if (request.getProductId() == null || request.getUserId() == null || request.getNum() == null) {
            return Result.error(ErrorCode.PARAMETER_ERROR);
        }

        // 查询商品信息
        Product product = productService.getProductById(request.getProductId());
        if (product == null) {
            return Result.error(ErrorCode.PRODUCT_NOT_EXIST);
        }

        // 检查库存情况
        if (product.getStock() < request.getNum()) {
            return Result.error(ErrorCode.STOCK_NOT_ENOUGH);
        }

        // 创建订单
        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setProductId(request.getProductId());
        order.setNum(request.getNum());
        order.setTotalFee(product.getPrice().multiply(new BigDecimal(request.getNum())));

        // 更新库存
        productService.decreaseStock(request.getProductId(), request.getNum());

        // 写入数据库
        orderService.addOrder(order);

        return Result.success(order);
    }

}

这个服务主要包含了控制器层、业务逻辑层和数据访问层三个部分。其中,控制器层负责接收请求并进行参数验证,业务逻辑层负责处理各种业务逻辑,数据访问层则负责与数据库打交道。

DDD 实现

在 DDD 实现中,我们可能会按照以下步骤进行:

  1. 分析业务场景和业务需求,根据实际情况设计相应的业务模型和领域模型。
  2. 领域模型包含了商品、订单、客户等相关信息,每个模型都有特定的属性和行为。
  3. 根据订单需求,编写相应的方法,例如创建订单、检查库存、更新订单状态等等。
  4. 使用业务逻辑进行订单的创建和处理,通过领域模型来描述和理解业务和系统的本质。
  5. 最后通过前端页面/接口等方式,完成下单操作。

在这种实现方式中,DDD 强调的是业务逻辑和领域模型的建模和设计,更加注重领域专家的意见和业务需求。因此,这种方式能够更加贴近业务需求,具有更好的可扩展性和可维护性。

总之,常用的分层实现方式注重于技术实现和体系结构的设计,DDD 则更注重于业务逻辑和领域模型的设计,让开发人员更容易理解和解决复杂业务问题。

具体代码:

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderApplicationService orderApplicationService;

    // 新增订单
    @PostMapping("/add")
    public Result addOrder(@RequestBody OrderRequest request) {
        OrderDto orderDto = orderApplicationService.createOrder(request);
        return Result.success(orderDto);
    }

}

@Service
public class OrderApplicationService {

    @Autowired
    private OrderFactory orderFactory;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ProductRepository productRepository;

    // 创建订单并保存订单信息
    public OrderDto createOrder(OrderRequest request) {
        // 使用工厂类创建订单对象
        Order order = orderFactory.createOrder(request);

        // 检查商品库存是否充足
        Product product = productRepository.findProductById(request.getProductId());
        if (product == null) {
            throw new RuntimeException("Product not exist!");
        }
        if (product.getStock() < request.getNum()) {
            throw new RuntimeException("Stock not enough!");
        }

        // 扣减商品库存量
        product.decreaseStock(request.getNum());

        // 保存订单信息到数据库
        orderRepository.saveOrder(order);

        // 返回订单DTO
        return order.toOrderDto();
    }

}

// 订单工厂类,用于创建订单对象
public class OrderFactory {

    public Order createOrder(OrderRequest request) {
        // 创建商品对象
        Product product = new Product(request.getProductId(), request.getNum());

        // 计算订单总价
        BigDecimal totalFee = product.getPrice().multiply(new BigDecimal(request.getNum()));

        // 创建订单对象
        return new Order(request.getUserId(), product, request.getNum(), totalFee);
    }

}

// 订单仓储接口,用于保存订单信息到数据库
public interface OrderRepository {

    void saveOrder(Order order);

}

// 商品仓储接口,用于查询商品信息
public interface ProductRepository {

    Product findProductById(Long productId);

}

在这个实现中,我们把业务逻辑和领域模型的设计放到了最重要的位置。 OrderApplicationService 负责调用 OrderFactory 来创建订单对象,并对订单进行一些操作,例如检查库存和扣减库存等事项。OrderFactory 负责创建 Order 对象。而 OrderRepository 和 ProductRepository 则负责数据持久化操作。

可以看出,DDD 的实现方式更加注重领域模型和业务逻辑的设计,尽可能把业务逻辑封装到领域模型里面,提高了代码的可读性和可维护性。同时也让我们更好地理解和解决复杂业务问题。

是怎么把业务逻辑封装到领域模型里面的?

在上面的例子中,订单对象 Order 是一个领域模型。它封装了订单相关的业务逻辑,例如:

  1. 计算订单总价(根据商品单价和数量计算得到)。
  2. 支付订单(将订单状态修改为已支付)。
  3. 取消订单(将订单状态修改为已取消,恢复商品库存等)。

在领域驱动设计中,是将业务逻辑尽可能地放到领域模型里。这样可以让业务逻辑和数据尽量内聚,降低模块之间的耦合度,提高代码的可维护性和可扩展性。

在上面的例子中,通过调用 orderFactory.createOrder(request) 方法创建订单对象;通过调用 order.pay()order.cancel() 方法完成订单的支付和取消操作。

此外,在 OrderApplicationService 服务类中还完成了扣减商品库存、保存订单信息等操作。这些业务逻辑也可以进一步封装到 Order 类的方法或其他领域模型中,以达到领域驱动设计的目标。

总结

2024.5.1 微信公众号看到这篇文章,又学到了一些东西,在这篇博客里补充一下。DDD这玩意还挺复杂的。这篇文章的大佬说的对:“DDD的设计能力是在实践中逐步积累的,不要害怕一上来不得其法,这是肯定的。如果工作中没有这样的机会,你可以给自己模拟一些场景来试试手”。大佬还推荐了一些文章:
《领域驱动设计DDD|从入门到代码实践》
《阿里技术专家详解 DDD 系列》

某一天??? 粗略的了解一下DDD。可能之前没怎么用过,所以感觉整体还是比较抽象。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值