对DDD领域驱动模型的粗略理解

0 阅读21分钟

DDD领域驱动模型

  • DDD是什么——DDD是三层架构的升级版,有助于解决系统老化问题

    传统的三层架构(表现层、业务逻辑层、数据访问层)或MVC模式,在系统建设初期往往简单高效。但随着业务复杂度的提升,系统容易陷入“代码泥潭”:业务逻辑分散在各层,数据与行为分离(贫血模型),导致系统难以维护、扩展,最终出现“系统老化”现象。

    领域驱动设计(DDD)并非完全抛弃分层,而是对传统架构的升级与重构。它将关注点从“技术实现”转移到“业务领域”,通过建立反映业务本质的领域模型,确保代码结构随着业务的发展而持续演进,从而有效遏制系统老化,保持系统的生命力。

  • DDD的业务优先

    DDD与MVC/三层架构的核心分歧在于驱动力的不同:

    MVC/三层架构(技术优先) : 关注点:侧重于代码的组织形式和技术分层(如Controller、Service、DAO)。 痛点:业务逻辑往往被淹没在技术细节中。业务人员看到代码中的“类”和“表”时一头雾水,无法理解系统是如何运作的。这导致业务人员完全无法参与技术讨论,只能被动等待功能交付。

    DDD(业务优先) : 关注点:侧重于业务逻辑、领域规则和业务流程。代码结构直接映射业务概念(如“订单”、“支付”、“库存”)。 优势:降低沟通壁垒,促进业务参与。 统一语言:开发人员使用的类名、方法名与业务人员口中的术语完全一致。 可视化业务:即使是不懂代码的业务人员,看到“订单聚合”、“支付领域服务”这样的结构,也能大致理解系统的核心脉络。 共同设计:业务人员不再是需求的“甩手掌柜”,而是能参与到领域模型的构建中,确保软件真正解决业务问题。

  • DDD以一系列抽象概念为开发模式

    是的,确实很抽象,充满了诸如“聚合根”、“限界上下文”、“防腐层”等抽象术语。这些词很抽象,但目的不是为了创造新词汇,目的是为了解决“怎么让写出来的代码,真正符合复杂的业务逻辑,而不是变成一团乱麻。 ”这一问题。

    这些抽象概念主要分为两种:战略设计战术设计

    战略设计

    战略设计在宏观角度来看就是给项目分地盘,是用来划分边界的。主要包含四个抽象概念:

    • 领域与子域:领域是软件系统所要解决的全部业务问题和规则的集合,它定义了我们业务的边界和范围。例如,电商系统的领域涵盖了从商品展示、用户下单、支付处理到物流配送的全过程。面对一个庞大而复杂的领域,直接进行系统设计会异常困难。因此,DDD采用“分而治之”的策略,将一个大的领域分解为多个更小、更易管理的子域。 子域的划分并非随意进行,而是根据其对业务的核心价值和战略重要性,将其分为三类:

      • 核心域:这是业务的生命线,是企业区别于竞争对手的核心竞争力和差异化优势的来源。例如,电商平台的智能推荐算法、金融公司的风控模型。对于核心域,我们必须投入最优秀的团队和资源进行自研,构建技术壁垒。
      • 支撑域:这是业务正常运行所必需的功能,但它本身不构成核心竞争力,其主要职责是支撑核心域的运作。例如,电商的库存管理、物流调度。对于支撑域,我们可以选择自研,但目标是“稳定可用”,无需追求业界领先,也可以考虑使用成熟的第三方服务。
      • 通用域:这是几乎所有软件系统都需要的、高度标准化的通用功能,与核心业务逻辑无关。例如,用户认证、支付网关对接、邮件通知服务。对于通用域,最佳策略是“能买就买,能用开源就用开源”,坚决避免重复造轮子,以最小化成本和开发精力。
    • 统一语言为技术核心:就是说可以让技术人员和业务人员包括测试人员都可以用相同的“频率”来交流,不至于出现说是客户的目的是做一个秋千,而技术人员和业务人员对这个秋千的理解不一样,从而设计的秋千也是千奇百怪,导致难以统一一起,难以满足客户需求。以统一语言为技术核心,强调的是要在每一个项目内形成一个通用的语言。 这不仅仅是包结构类似,更重要的是命名的一致性。统一语言必须体现在代码的命名(类名、方法名、变量名)、数据库表名以及测试用例名中。如果业务方说“下订单”,代码里就必须是 placeOrder 而不是 createOrdersaveOrder代码即文档,看到代码中的类名和方法名,就应该能直接读懂业务逻辑。

    • 限界上下文:意思是同一个词,在不同的业务场景里,含义是不一样的,这里的不同业务场景就是上下文。我们在实现这一概念的时候,需要把一个系统切分为不同的多个上下文,例如在销售上下文中,商品这一词汇我们关心的是价格和消息,而在物流上下文中,商品的关注点就变为了重量体积等。 关键点:在代码层面,这通常意味着两个完全不同的类。销售上下文有一个 SalesProduct 类(含价格字段),物流上下文有一个 ShippingProduct 类(含重量字段)。限界上下文强调的是模型和逻辑上的彻底隔离,千万不要试图用一个巨大的类包含所有字段。

    • 上下文映射:这个的意思就是上下文的交流,刚刚进行了上下文限界,分割了多个地盘,但是分好后地盘之间需要交流。比如说订单系统的客户需要查询库存,而库存系统的供应商提供查询接口。两个系统的交互就是上下文映射。

    战术设计

    战术设计则是在微观的角度进行堆积拼接,而DDD提供了很多拼接的原材料:

    • 实体:有唯一身份证id的对象。不会因为跨越上下文跨越领域而改变其本质(id)。

      PS:虽然实体的本质不变,但在跨越上下文时,我们通常不直接共享实体对象。比如“用户中心”的ID可能是UUID,但“订单中心”可能只用一个“客户编号”来引用。跨越边界时,通过ID或DTO进行引用,保持模型的独立性。

    • 值对象:没有唯一身份证的,只描述属性且不变的对象。就比如说刚刚提到的实体的其他属性,比如一个人身上的穿的衣服这一属性,一个属性就是一个值对象。

    • 聚合与聚合根:聚合就是把一群强关联性的实体和值对象捆绑在一起形成一个集合(事务边界),聚合根就是这个集合的特殊实体,访问聚合的唯一入口,就是集合的老大。比如说购物的订单就是聚合根,其中的很多订单项就是实体,订单项的金额就是值对象。

    • 领域服务:总有一些业务逻辑不属于任何一个实体,比如说转账涉及双方,无法将这个行为写入任何一个单一的一方,而领域服务就会进行承载。

    • 领域事件:这是解耦的神器,也是连接不同限界上下文的重要纽带。 当一个业务动作完成时(如“订单已支付”),系统会发布一个事件。其他上下文(如积分系统、库存系统)可以订阅这个事件并做出反应(加积分、扣库存),而无需直接调用对方的接口。这实现了系统间的异步解耦

    • 资源库(又叫仓库):这个就是主要负责和数据库对接,把聚合放入或取出数据库,对其他业务而言,他就是数据库的代理。(-----PS : 资源库 Repository 是聚合的集合。它屏蔽了数据库的存在,让领域层感觉像是在操作内存中的对象集合(List),而不是直接操作数据库表。)

    ----- 注意以下的区分

    领域服务:负责处理那些不属于单一实体的复杂业务规则(如转账时的汇率计算、跨聚合的校验)。

    应用服务:负责协调和编排(如开启事务、调用领域服务、保存数据)。 ​ 不要混淆两者,领域服务专注于“ 业务逻辑 ”,应用服务专注于“ 流程控制 ”。


    以下是更为详细的针对流程的描述


    实体

    与以往的(MVC)实体不同,DDD中的实体类中是可以写入方法的。在以往的实体类有属性和get/set的基础上写入部分业务逻辑,这种实体被称之为充血模型,与其对应的就是以往的没有业务逻辑的 贫血模型(POJO)

    • 核心特征:实体不仅要有业务逻辑,最重要的是它拥有唯一标识(ID) 。即使两个对象的属性(如姓名、金额)完全一样,只要ID不同,它们就是两个不同的实体。
    • 写入原则:并不是所有的业务逻辑都可以写入,只能写入引起属性状态变化的方法。例如:账户的转账,引起了余额这一属性的变化,就可以写入。
    • 模型独立性:这让我们的实体类不再完全对应数据库表中的结构。我们可能在一个实体中只写入与当前业务相关的属性,比如银行核心系统关注余额、利息、账户状态;而积分系统关注这个账户攒了多少积分,能不能兑换礼品。这涉及到聚合和限界上下文概念,等下再讲。而对于与数据库的交互,就用到下面提及的资源库这一概念了。
    资源库

    还是和以前一样抽象出一个接口,然后再用实现类来实现对应的与数据库交互的具体过程。这样的好处大大的有:

    • 集合隐喻:资源库让领域层感觉像是在操作内存中的集合(Collection) ,完全屏蔽了数据库的存在。
    • 高内聚低耦合:通过这种方式,首先是实现了高内聚低耦合的思想,其他业务和它的变更相互之间影响少。
    • 扩展能力:未来如果要换一个数据库技术(比如把 Hibernate 换成 MyBatis),或者数据库表结构变了,只需要修改资源库的实现类,核心业务代码(领域层)完全不用动。
    值对象

    这里要强调一下值对象这个概念。前面提到值对象是一个只描述属性且不变的对象,这个意思是说值对象就是一个类,一个属性类。对应实体类,实体类有id,还有属性,其引用的属性可以就是一个值对象。

    • 无副作用:值对象内部也可以写入一些业务逻辑方法,但不涉及自身数值的变化(不可变)。比如一个值对象叫金额,其内部业务逻辑可以是金额相加,返回一个新的金额对象,而不是修改原来的金额。
    • 可替换性:因为没有ID,改变值对象通常是“整体替换”,比如把“地址A”换成“地址B”。
    防腐层

    这个只是一个设计模式,主要是隔离外部系统(如遗留系统、第三方服务或其他微服务)的模型和接口,防止其“污染”或“侵蚀”你当前系统的领域模型。它不管其他第三方接口怎么实现业务的,它只负责调用接口,然后接收最后的结果。就算未来换了第三方接口,那也无所谓,它只负责要处理这个业务的结果。

    PS:这个名字很有意思,哪怕其他的第三方服务做的烂的一比,对我们这个系统也是没有任何影响,我们只要调用后的结果。

    领域服务

    针对一些业务场景比如:商家商品标售10元,客户付出12元,商家到手7元。这种涉及多个实体且不适合放在某一个单独实体中的业务时,采用领域服务。

    • 形式:在形式上也是抽象出一个接口,在这个接口中进行交易(注意,DDD建议这个接口中的方法传参都是实体)。
    • 职责:强调跨多个实体的业务动作交由实体自己去完成,这个实现类只负责将各个实体组装成一个业务场景,把这些实体的动作抽象成表示层的业务逻辑。比如让客户扣多少钱,客户实体自己算;然后商家得到多少钱也是自己去算。领域服务只负责协调和计算,不负责保存数据。
    限界上下文

    解决的是“类外部太乱”的问题,也就是“分家”的问题。在传统开发中,我们常常会遇到一个数据库表对应一个庞大实体类的情况,这个类包含了所有业务场景可能用到的属性和行为,导致耦合度极高。而DDD通过限界上下文,为不同的业务场景划定清晰的边界。

    • 例子:例如,在银行核心系统中,我们关注的是账户的余额、利息等金融属性;而在积分系统中,我们关注的是账户的积分余额、会员等级等忠诚度属性。这两个“账户”虽然都叫Account,但它们属于不同的限界上下文,拥有完全不同的职责和模型。
    • 语言边界:同一个词在不同上下文意思可能不同(比如“客户”在销售上下文是“买家”,在物流上下文是“收货人”)。限界上下文允许我们针对不同的业务场景,创建专属的、职责单一的Account类,每个类只包含当前场景所需的属性和行为,从而实现了模型的高内聚和低耦合。
    聚合

    解决的是“类内部太乱”的问题,也就是“管家”的问题。当我们通过限界上下文确定了一个具体的业务场景(比如银行核心系统中的账户)后,这个Account类内部可能依然会很复杂,包含余额、地址、交易记录等多个紧密关联的部分。

    • 定义:聚合就是将这个Account(作为聚合根)及其紧密相关的值对象(如Money、Address)和实体(如Transaction)封装成一个不可分割的整体。

    • 交互规则:聚合规定了所有对外部世界的交互都必须通过聚合根(Account)来进行,从而确保了聚合内部数据的一致性和业务规则的完整性。例如,修改账户地址必须通过Account的changeAddress方法,而不是直接操作Address对象,这样可以保证在修改地址的同时,可以触发相关的业务逻辑。

    • 事务边界(重要补充)聚合是数据修改的事务边界。 在一次业务操作中,为了保证数据强一致性,我们只能修改一个聚合内的对象。如果要修改另一个聚合,通常需要通过“领域事件”来异步通知。

    • 判断标准:那怎么确定值对象和实体之间是否需要聚合呢?就看失去这个值对象的时候,值对象在业务中是否就没有作用了。

    PS限界上下文和聚合是包含与被包含的关系。一个限界上下文内部可以包含一个或多个聚合。限界上下文为聚合提供了统一的语义边界和业务语境,而聚合则在限界上下文内部,负责维护数据和业务规则的一致性。

  • DDD以四层架构为基本思想:

    DDD的四层架构是其战略设计的核心体现,旨在通过清晰的分层实现业务逻辑与技术细节的彻底解耦,构建一个高内聚、低耦合、易于维护和演进的软件系统。这四层分别是:用户接口层应用层领域层基础设施层

    用户接口层

    用户接口层是系统与外部世界交互的门户,其职责纯粹而明确:接收外部指令,并返回处理结果。

    • 核心职责

      • 请求接收与响应:作为系统的入口,负责接收来自前端、其他微服务或第三方系统的HTTP、RPC等请求。
      • 数据格式转换:将外部传入的请求数据(如JSON)转换为应用层能够识别和处理的数据传输对象。在处理完毕后,再将应用层返回的结果转换为外部调用方所需的格式(如视图对象或API响应)。
      • 基础校验与认证:处理一些与业务逻辑无关的通用任务,例如请求参数的格式校验(非空、长度等)、用户身份认证(Token验证)和权限检查。
    • 关键原则

      • 不包含任何业务逻辑:这一层只负责“翻译”和“转发”,绝不应包含“订单金额如何计算”或“库存如何扣减”等业务规则。
      • 保持精简:其代码应尽可能简单,所有复杂的流程都应委托给应用层处理。

    应用层

    应用层是整个业务流程的“编排者”或“指挥家”。它非常薄,不包含任何核心业务规则,其作用是协调领域层中的各个对象,以完成一个完整的业务用例。

    • 核心职责

      • 流程编排:根据业务场景,按特定顺序调用领域层的服务、聚合根或实体。例如,在一个“下单”场景中,它会依次协调“检查库存”、“创建订单”、“扣减余额”等步骤。
      • 事务管理:通常在这一层开启和提交数据库事务(例如使用@Transactional注解),以确保一个完整业务流程的原子性。
      • 驱动领域模型:它通过调用领域对象的方法来触发业务逻辑的执行,自身并不实现这些逻辑。它告诉领域模型“做什么”,而领域模型决定“怎么做”。
    • 关键原则

      • 不含业务规则:这是应用层最重要的原则。它不应包含任何条件判断或业务计算,如“如果用户是VIP则打八折”。这类逻辑必须下沉到领域层。
      • 作为协调者:它像一个项目经理,负责任务的分配和进度跟踪,但不亲自下场写代码。

    领域层

    领域层是整个DDD架构的“心脏”和灵魂,它承载着企业最核心的业务知识和业务规则。这一层是纯粹的业务逻辑表达,不依赖任何外部技术细节。

    • 核心职责

      • 实现核心业务逻辑:所有与业务相关的状态变更、规则计算、行为定义都集中于此。

      • 封装业务概念

        :包含了之前讨论的所有核心领域构建块:

        • 实体值对象:表达业务概念及其行为。
        • 聚合聚合根:定义数据一致性的边界和事务边界。
        • 领域服务:处理跨多个聚合的复杂业务逻辑。
        • 领域事件:用于在聚合之间或限界上下文之间发布业务状态变更的消息。
    • 关键原则

      • 业务优先:代码结构直接反映业务语言和流程,使业务人员能够理解。
      • 技术无关:领域层不应直接依赖数据库、消息队列或任何第三方API。它只关心业务本身。

    基础设施层

    基础设施层为上层提供技术支撑,负责处理所有与技术细节相关的操作,如数据持久化、消息发送、调用外部服务等。

    • 核心职责

      • 数据持久化:实现数据的存储和读取。
      • 第三方集成:封装对邮件服务、短信网关、支付接口等外部系统的调用。
      • 消息传递:实现消息队列的生产者和消费者。
      • 通用工具:提供日志记录、缓存访问等通用技术能力。
    • 关键原则:依赖倒置

      • 资源库的特殊性:这是理解基础设施层与领域层关系的关键。资源库的接口定义在领域层,而其具体实现则在基础设施层
      • inversion of Control:这意味着领域层定义“我需要如何获取和保存数据”(通过接口),而基础设施层负责“具体如何去数据库或Redis中操作”(通过实现)。这样,核心的业务逻辑就完全摆脱了对具体数据库技术的依赖。

    数据流转与依赖关系

    理解四层架构,必须清晰地把握数据是如何在各层之间流动的,以及它们之间的依赖方向。

    • 依赖方向

      • 总体上是自上而下的依赖:用户接口层 → 应用层 → 领域层。
      • 但通过依赖倒置原则,领域层不直接依赖基础设施层,而是由基础设施层来依赖领域层(通过实现其定义的接口)。
    • 典型请求处理流程

      1. 用户接口层接收一个“创建订单”的HTTP请求,将其反序列化为一个CreateOrderDTO对象。
      2. 应用层OrderAppService接收到CreateOrderDTO
      3. 应用层通过OrderRepository接口(定义在领域层)加载相关的聚合根(如Customer)。
      4. 应用层调用Customer聚合根的placeOrder()方法,传入订单信息。
      5. 领域层Customer实体内部执行业务逻辑(检查信用、创建Order实体等),状态发生变更。
      6. 应用层再次通过OrderRepository接口,将变更后的Customer和新的Order聚合根保存。
      7. 基础设施层OrderRepositoryImpl接收到保存请求,执行具体的SQL操作,将数据写入MySQL数据库。
      8. 应用层提交事务,并将结果返回给用户接口层
      9. 用户接口层将结果封装成JSON响应,返回给客户端。
  • DDD的弊端:类爆炸

    DDD确实存在一个显著的弊端,即类爆炸

    • 现象:原本在传统架构中可能只需要一两个类就能解决的业务流程,在DDD中可能需要从中抽出很多类(如实体、值对象、领域服务、领域事件等)来完成。
    • 前期成本:前期的搭建工作非常麻烦,需要投入大量精力进行领域建模和类的设计。
    • 后期收益:但当系统体量起来以后,维护变得很轻松,且代码结构清晰,更容易理解复杂的业务逻辑。这是一种“先苦后甜”的投入。
  • ️ 架构演进:单体优先原则

    好的项目通常都遵循单体优先的原则。

    • 演进路径:项目往往先从一个单体架构开始,随着业务逐步发展变大,复杂度提升后,再利用DDD的思想将其拆分为微服务架构。
    • 失败教训:那些试图一开始就直接做微服务架构的项目,往往因为过早地引入了分布式系统的复杂性,而最终以失败告终。DDD和微服务是解决复杂性的手段,而非项目启动时的标配。