Skip to content

lxy-l/DDD

Repository files navigation

Domain-Driven Design 理解

对于领域驱动设计思想的学习与理解。


战略设计

战略设计是从宏观的角度对业务进行领域划分和构建领域模型,梳理出核心域、子域、限界上下文和统一的领域语言。 对业务进行领域划分,构建领域模型,梳理出限界上下文,聚合,实体,值对象
一个领域就是一个问题空间,我们在业务中所遇到的所有的问题与挑战

建模方法

  1. 四色建模
  2. 限界笔纸
  3. 事件风暴
  4. 用户故事

战术设计

战术设计是从微观的角度对领域模型进行细化和实现,梳理出聚合、实体、值对象、工厂、仓储、服务、事件等概念,并定义它们的属性和行为。 以领域模型为基础,以限界上下文作为微服务划分的边界进行微服务拆分,实现对领域模型对于代码的映射
将战略设计进行具体化和细节化,它主要关注的是技术层面的实施 一个领域就算一个解决问题空间,用来解决在问题空间的所有问题;


分层

Domain 领域层

DDD概念中的核心业务层,封装所有业务逻辑,包含entity、value object、domain service、domain event等。

  1. Entity(Reference Object):

    由标识定义的实体。

  2. Value Object:

    描述了一个事务的某种特征。用于描述领域的某个方面而本身没有概念标识的对象(值对象)实例化之后表现一些设计元素,对于这些元素,只关心他是什么,而不关心他是谁。当我们关心一个模型的属性时,应把他归类为Value Object。他是不可变得,不需要任何标识

  3. Aggregate Root:

    聚合是用来封装真正的不变性,而不是简单的将对象组合在一起;
    聚合应尽量设计的小; 聚合之间的关联通过ID,而不是对象引用; 聚合内强一致性,聚合之间最终一致性;

  4. Aggregate:

    聚合是一种用来封装真正的不变性的模式,它由一个或多个实体,值对象和一个聚合根组成。聚合根是一个特殊的实体,它负责维护聚合内部的一致性和完整性,并提供对外访问的接口。聚合的作用是将相关的对象组织在一起,形成一个有意义的整体,并保证其内部状态不被外部干扰或破坏。聚合应该设计为尽可能小,只包含必要的对象和行为,以降低复杂度和提高性能。

    聚合是在限界上下文中识别出来的,限界上下文是一个显示的边界,领域模型便存在于这个边界之内。限界上下文可以通过以下几个步骤来识别:

    • 确定业务愿景:明确业务目标、价值、范围和边界。
    • 划分核心域:找出业务中最重要、最具竞争力、最有价值的部分,并将其作为核心域。
    • 划分周边子域:找出业务中次要、支持、通用的部分,并将其作为周边子域。
    • 定义限界上下文:根据子域划分出不同的限界上下文,并给出清晰的名称和描述。
    • 建立上下文映射:确定不同限界上下文之间的关系和依赖,并制定相应的协作策略。
  5. DomainService:

    领域层中的服务,负责检查是否满足临界值。例如银行转账功能。应属于领域服务,因为它包含重要业务逻辑。
    如果某些操作不属于任何一个实体或值对象,而是涉及到多个对象之间的协作或逻辑,那么可以将这些操作封装为领域服务,并放在相应的聚合内

    1. 与必要的账户和总账对象进行交互,执行相应的借入贷出操作
    2. 提供结果确认(允许或拒绝)
  6. DomainEvent

    如果某些操作会导致聚合内部状态发生变化,并且这种变化对其他限界上下文有意义,那么可以将这种变化发布为领域事件,并放在相应的聚合内2。

  7. Domain Factory:

    如果创建一个复杂的聚合需要很多步骤和逻辑,那么可以将这些步骤和逻辑封装为工厂方法,并放在相应的聚合内。

  8. Domain Specification:

    如果需要对一个复杂的业务规则进行判断或验证,那么可以将这些规则封装为规约,并放在相应的聚合内。

  9. Domain Strategy:

    负责封装领域的可变行为,通常是一些需要根据不同的场景或配置选择不同的算法或实现的情况。

需要注意的地方:

  • 领域层应该包含业务的核心逻辑和规则,以及业务的概念和术语,以反映业务的本质和价值。
  • 领域层应该使用领域驱动设计的方法和模式,例如实体、值对象、聚合、领域服务、领域事件等,以构建富有表现力和一致性的领域模型。
  • 领域层应该遵循单一职责原则,将领域划分为不同的子域和限界上下文,以实现高内聚低耦合的设计。
  • 领域层应该遵循迪米特法则,尽量减少对外部层的依赖和交互,以保护领域的完整性和封装性。

Infrastructure 基础设施层

提供公共组件,如:Logging、Trascation、HttpClient,ORM等。
提供领域层所需的技术服务,例如持久化、消息、缓存、日志等。它是领域层与外部资源的适配器,实现了领域层定义的接口

  1. Repository

    负责将领域对象持久化到数据库或其他存储介质中,以及从存储介质中查询领域对象。

  2. Unit of Work

    负责管理多个仓储操作的事务性,保证数据的一致性。

  3. Domain Event

    负责发布和订阅领域层产生的事件,以及将事件转发到消息队列或其他通道中。

  4. Service Proxy

    负责调用或提供外部服务接口,例如RPC、HTTP、SOAP等。

  5. Utility

    负责提供一些通用的功能,例如加密、压缩、序列化等。

需要注意的地方:

  • 基础设施层应该实现领域层定义的接口,例如仓储、领域事件、服务代理等,以提供技术服务给领域层。
  • 基础设施层应该封装技术细节,例如数据库操作、消息发送、外部服务调用等,以降低领域层的复杂度和依赖性。
  • 基础设施层应该使用依赖反转的原则,让领域层控制基础设施层的行为,而不是相反。
  • 基础设施层应该遵循开闭原则,对扩展开放,对修改关闭,以适应不同的技术选型和业务需求。

Application 应用层

应用层的作用是组织业务场景,编排业务,隔离场景对领域层的差异。它是应用服务的提供者,负责协调领域层和基础设施层的交互,处理用户命令和展示。

ApplicationService应该永远返回DTO而不是Entity
1.构建领域边界
2.降低规则依赖
3.通过DTO组合降低成本

  1. Application Service

    负责组合领域服务、领域事件、仓储等,完成具体的业务逻辑,提供完整的业务服务。

  2. Assembler

    负责将领域对象转换为数据传输对象(DTO),或者将DTO转换为领域对象,实现领域层和外部层的数据适配。

  3. DTO

    负责封装应用层向外部传输的数据,或者接收外部传入的数据,实现数据的序列化和反序列化。

需要注意的:

  • 应用层不应该包含业务规则或知识,而应该委托给领域层处理。
  • 应用层应该保持尽可能薄的,只负责协调和委托,避免过度封装或抽象。
  • 应用层应该使用依赖注入的方式,松耦合地依赖于领域层和基础设施层的接口,而不是具体的实现。
  • 应用层应该使用数据传输对象(DTO)作为输入和输出,而不是领域对象,以保护领域的完整性和封装性。
  • 应用层应该使用装配器(Assembler)来转换DTO和领域对象,而不是在领域对象中暴露构造函数或属性。

WebApi/MVC 展现层

展现层的作用是负责向用户展示应用的界面,接收用户的输入,调用应用层的服务,处理异常和错误,以及提供安全和权限控制。
展现层可以有多种实现方式,例如Web页面,移动端应用,桌面应用,命令行界面等。
展现层应该尽量简单,不包含业务逻辑或数据访问代码,而是将这些逻辑委托给应用层和领域层。


总结

DDD概念

  1. 领域模型由领域专家和开发人员交流建模

  2. 应该领域层调用应用层。认为领域层是被驱动调用的,还是静态数据驱动思维。

  3. 战略设计和战术设计是相辅相成的,战略设计为战术设计提供了指导和范围,战术设计为战略设计提供了反馈和验证

DDD适用于以下场景:

  1. 业务较为复杂,模块概念较多,需要清晰地划分业务边界和解耦业务。
  2. 微服务边界划分困难,需要按照领域模型和限界上下文进行拆分。
  3. 技术异构的功能,需要按照技术边界进行拆分。

DDD有以下优点和缺点:

优点:

  • DDD可以更好地理解和沟通业务需求,建立丰富的领域模型和统一的领域语言。
  • DDD可以更快地拆分微服务,实现系统架构适应业务的快速变化。
  • DDD是一套完整而系统的设计方法,能够从战略设计到战术设计提供标准的设计过程。
  • DDD可以降低服务的耦合性,提高软件的质量和可维护性。
  • DDD善于处理高复杂度的业务问题,有利于领域知识的传递和传承。

缺点:

  • DDD需要花费更多的时间和精力去分析和建模领域,可能会影响开发效率。
  • DDD需要有较高的业务抽象能力和面向对象编程能力,对开发者要求较高。
  • DDD可能会导致过度设计或过度封装,使得代码难以阅读或修改。

聚合根、实体、值对象:

1.聚合根、实体、值对象的区别?

从标识的角度:

聚合根具有全局的唯一标识,而实体只有在聚合内部有唯一的本地标识,值对象没有唯一标识,不存在这个值对象或那个值对象的说法;

从是否只读的角度:

聚合根除了唯一标识外,其他所有状态信息都理论上可变;实体是可变的;值对象是只读的;

从生命周期的角度:

聚合根有独立的生命周期,实体的生命周期从属于其所属的聚合,实体完全由其所属的聚合根负责管理维护;值对象无生命周期可言,因为只是一个值;

2.聚合根、实体、值对象对象之间如何建立关联?

聚合根到聚合根:通过ID关联;

聚合根到其内部的实体,直接对象引用;

聚合根到值对象,直接对象引用;

实体对其他对象的引用规则:1)能引用其所属聚合内的聚合根、实体、值对象;2)能引用外部聚合根,但推荐以ID的方式关联,另外也可以关联某个外部聚合内的实体,但必须是ID关联,否则就出现同一个实体的引用被两个聚合根持有,这是不允许的,一个实体的引用只能被其所属的聚合根持有;

值对象对其他对象的引用规则:只需确保值对象是只读的即可,推荐值对象的所有属性都尽量是值对象;

3.如何识别聚合与聚合根?

明确含义:一个Bounded Context(界定的上下文)可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根;

识别顺序:先找出哪些实体可能是聚合根,再逐个分析每个聚合根的边界,即该聚合根应该聚合哪些实体或值对象;最后再划分Bounded Context;

聚合边界确定法则:根据不变性约束规则(Invariant)。不变性规则有两类:1)聚合边界内必须具有哪些信息,如果没有这些信息就不能称为一个有效的聚合;2)聚合内的某些对象的状态必须满足某个业务规则;

不同的聚合之间的协作需要遵循一些原则,比如 :

  1. 尽量减少不同聚合之间的依赖,避免形成紧密耦合。
  2. 不要直接修改或访问其他聚合内部的状态,而是通过聚合根暴露的接口或方法进行交互。
  3. 不要在一个事务中同时操作多个聚合,而是使用领域事件或消息队列等异步机制来保证最终一致性。
  4. 不要在一个聚合中引用其他聚合的实体或值对象,而是使用标识符或快照等方式来表示关联关系。

领域服务是一种封装一些跨越多个聚合或实体的业务逻辑的模式,它通常是无状态的,并且只依赖于领域对象。领域服务应该遵循以下几个原则:

  • 尽量少用:只有当业务逻辑无法归属于任何一个聚合或实体时,才考虑使用领域服务。
  • 保持纯粹:只包含与业务相关的逻辑,不包含技术细节或基础设施。
  • 避免重复:不要在多个地方实现相同的业务逻辑,而是将其抽象为一个领域服务。
  • 遵循依赖倒置:不要直接依赖于具体的实现类,而是依赖于抽象的接口或者规范1。

领域事件是一种表示领域中发生了某种事情的对象,它通常是一个值对象,并且包含了事件发生时的相关数据。领域事件可以用来在聚合之间传递信息,解耦业务流程,触发后续操作等。领域事件应该遵循以下几个原则:

  • 反映业务价值:只有对业务有意义并且会导致进一步操作的事件才应该被建模为领域事件23。
  • 使用过去时态:因为领域事件表示已经发生了的事实,所以它们应该使用过去时态来命名43。
  • 保持简单和清晰:不要在一个领域事件中包含过多或者无关的数据,只保留必要和最小化的信息3。
  • 支持序列化和反序列化:为了能够在不同限界上下文之间传输和处理领域事件,它们应该能够被序列化和反序列化4。

设计和实现领域服务和领域事件需要根据具体的业务场景和技术选型来进行。一般来说,可以参考以下几个步骤:

通过用户旅程图、场景分析、通用语言等方法来识别出潜在的领域服务和领域事件3。 通过定义接口、规范、值对象等方式来建模出所需的

// 使用code blocks语法来封装长格式内容
public class Order {
  // 订单ID,唯一标识一个订单
  private String orderId;
  // 订单状态,枚举类型
  private OrderStatus status;
  // 订单总金额,值对象
  private Money totalAmount;
  // 订单项列表,值对象集合
  private List<OrderItem> items;
  
  // 构造函数,创建一个新的订单
  public Order(String orderId, List<OrderItem> items) {
    this.orderId = orderId;
    this.status = OrderStatus.CREATED; // 初始状态为创建
    this.items = items;
    this.totalAmount = calculateTotalAmount(); // 根据订单项计算总金额
  }
  
  // 计算订单总金额的方法,返回一个Money值对象
  private Money calculateTotalAmount() {
    Money total = new Money(0); // 初始金额为0
    for (OrderItem item : items) { // 遍历每个订单项
      total = total.add(item.getSubtotal()); // 累加每个订单项的小计金额
    }
    return total;
  }
  
  // 支付订单的方法,改变订单状态为已支付,并触发支付事件(省略具体实现)
  public void pay() {
    if (status == OrderStatus.CREATED) { // 只有创建状态的订单才能支付
      status = OrderStatus.PAID; // 改变状态为已支付
      publishPaymentEvent(); // 发布支付事件(省略具体实现)
    } else {
      throw new IllegalStateException("Only created order can be paid"); // 抛出非法状态异常
    }
    
  }
  

About

领域驱动设计分层概念

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published