1.前言

  • DDD(Domain-driven design)领域驱动设计是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法。领域模型是对业务模型的抽象,DDD是把业务模型翻译成系统架构设计的一种方式。
  • DDD与微服务的区别:
    • DDD的核心诉求是将业务架构映射到系统架构上,在响应业务变化调整业务架构时,也随之变化系统架构。
    • 微服务追求业务层面的复用,设计出来的系统架构和业务一致;在技术架构上则系统模块之间充分解耦,可以自由地选择合适的技术架构,去中心化地治理技术和数据。
  • DDD的特点:
    • 根据业务模型设计系统:根据业务语义抽象梳理设计成领域模型,而不是通过数据库等数据源驱动设计。
    • 数据模型统一:通过真实业务背景,梳理出业务域模型自然会形成出参、入参、中间临时属性收口统一为域模型
    • 业务模型与数据源无关:数据源更换,领域模型无感知,无须变更;一个域模型底层可能对应n个数据源;系统升级底层数据源结构改造时,变更对业务层是透明,域模型可无缝对接。
    • 业务属性字段命名统一、引用唯一:MVC模式开发中,入参model/数据传输model/数据源model,同一个业务属性含义可能有多种不同的命名。
    • 业务行为Action收口:原有开发模式下,一个Model类是一个POJO、DTO、DO,仅做数据传输,没有任何业务相关Action,属于典型的贫血模型。DDD中一个Model表述一个业务的域,有属性、业务行为Action,并且这个域的所有操作都在这个Model中,不仅有数据传输的作用也是一个具体的Service,是属于充血模型。这就可以做到业务操作高内聚、低耦合,系统更能直观体现业务逻辑。
  • DDD优缺点:
    • 优点:系统演进更方便,分为业务复杂性变化的演进和业务数据量变化的演进;更方便测试
    • 缺点:系统改造成DDD复杂,开发熟悉DDD思想困难。
  • DDD适用场景
    • 已经过了系统初期生长阶段,稳定运行一段时间,支撑的业务前景明朗,需要进行一定重构的
    • 平台型业务系统,需要支持多类型、多租户、复杂功能的
    • 研发众多,时常会有外部新研发进来开发的
    • 核心包+插件化拓展的系统设计模式
  • 为什么要用DDD
    • 功能只是表象,模型才是内在
    • 好的模型可以让功能实现更容易

2.DDD常见名词

2.0 贫血模型与充血模型

2.0.1 区别

区别充血模型(DDD开发方式下贫血模型(传统OOP开发方式下)
编码方式以领域对象为主的行为状态转换事务脚本(CRUD、判断、循环、检查等逻辑只是简单的方法级别拆分)
Service跨领域逻辑的组合事务脚本的对外包装
Domain一个领域的entity集合包VO/DTO
Entity包含了一个领域里的状态、以及对状态的直接操作单元,具有自闭环的对象。VO/DTO
ValueObject简单的基本值对象,如电话号码、地址等,组成entity的基本单元语言基本类型
Aggregation(有点类似数学中集合的定义)将实体和值对象划分为聚合并围绕着聚合定义边界。多个VO/DTO
Aggregation Root唯一可以给外界作为某个Aggregation引用的entity。单个VO/DTO

2.0.2 优缺点

  • 贫血模型的优缺点

    • 优点1:很传统的编程思路,被许多程序员所掌握,许多教材采用的是这种模型,对于初学者,这种模型很自然,甚至被很多人认为是java中最正统的模型。
    • 优点2:思路清晰,事务边界清晰一般来说service的每个方法都可以看成一个事务,因为通常Service的每个方法对应着一个用例
    • 缺点:对象状态与行为分离,不能直观地描述领域对象。行为的设计主要考虑参数的输入和输出而非行为本身,不太具有面向对象设计的思考方式。
  • 充血模型的优缺点

    • 优点:贫血model偏重个性化,面向过程式。 充血偏共性化,采用OO设计,类拥有其属性及对应的行为,通过将职责分配到相应的模型对象或Service,可以很好的组织业务逻辑,因此非常适合于复杂的企业业务逻辑的实现,以及可复用程度比较高。
    • 缺点1:领域驱动建模要求对领域模型完整而透彻的了解,只给出一个用例的实现步骤是无法得到领域模型的,这需要和领域专家的充分讨论。错误的领域模型对项目的危害非常之大,而实现一个好的领域模型非常困难。
    • 缺点2:对象高度自洽的结果是不利于大规模团队分工协作。一个编程个体至少要完成一个完整业务逻辑的功能。对于单个完整业务逻辑,无法再细分下去了。

2.1 Model

2.1.1 Model定义

  • Model(模型):承载着业务的属性和具体的行为,是业务表达的方式,是DDD的内核。是一个类中有属性、属性有Get/Set方法,并且业务的行为(Action)操作也是在模型类中(充血模型)即做业务逻辑处理,又做数据传输对象,模型分为Entity、Value Object、Service这三种类型

2.1.2 Model分类

  • Entity (实体)
    • 有特定的标识,标识着这个Model在系统中全局唯一
    • 内部值可以是变化的,可能存在生命周期 (比如订单对象,状态值是连续变化的)
    • 有状态的Value Object
  • Value Object (值对象)
    • 内部值是不变的,不存在生命周期 (比如地址对象不存在生命周期)
    • 无状态对象
  • Service (服务)
    • 无状态对象
    • 当一个属性或行为放在Entity、Value Object中模棱两可或不合适的时候就需要以Service的形式来呈现

三种模型复杂度:Service > Entity > ValueObject,优先选择简单模型

2.2 生命周期

  • Factory (工厂):用来创建Model,以及帮助Repository (数据源)注入到Model中
  • Aggreagte (聚合根):封装Model,一个Mode中l可能包含其他Model(类似一个对象中包含其他对象的引用,实际概念更复杂)
    • 聚合是用来封装真正的不变性,而不是简单的将对象组合在一起;
    • 聚合应尽量设计的小;
    • 聚合之间的关联通过ID,而不是对象引用;
    • 聚合内强一致性,聚合之间最终一致性。
  • Repository (数据源):数据源的访问网关层、通过Repository来对接不同的数据源

2.3 边界

2.3.1 限界上下文

  • 域的拆分

    • 按业务抽象进行划分
    • 一个业务拆分成几个独立的域,每个域又可细拆成不同子域
  • 防腐

    • 一个域在访问其他域的模型时,把获取到的模型做层转换映射到自己域的模型中(不直接使用别的域模型作为自己域模型中的一部分)
    • 防止源域模型发生变更,依赖源域模型的调用方,在需要源域模型新功能时,必须要全局依赖修改,才在能兼容
    • 防止域上下文不一致产生的冲突
  • 限界上下文之间的映射关系

    • 合作关系(Partnership):两个上下文紧密合作的关系,一荣俱荣,一损俱损。
    • 共享内核(Shared Kernel):两个上下文依赖部分共享的模型。
    • 客户方-供应方开发(Customer-Supplier Development):上下文之间有组织的上下游依赖。
    • 遵奉者(Conformist):下游上下文只能盲目依赖上游上下文。
    • 防腐层(Anticorruption Layer):一个上下文通过一些适配和转换与另一个上下文交互。
    • 开放主机服务(Open Host Service):定义一种协议来让其他上下文来对本上下文进行访问。
    • 发布语言(Published Language):通常与OHS一起使用,用于定义开放主机的协议。
    • 大泥球(Big Ball of Mud):混杂在一起的上下文关系,边界不清晰。
    • 另谋他路(SeparateWay):两个完全没有任何联系的上下文。

3.DDD架构设计

3.1 DDD架构

  • 领域驱动设计没有特定的架构风格,它的核心是域模型驱动业务的思想,常见的领域驱动设计架构有传统的四层架构模式、事件驱动架构、CQRS架构、六边形架构等。

3.1.1 四层架构

  • User Interface为用户界面层(对外访问层API),负责向用户显示信息和解释用户命令。

  • Application为应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。

  • Domain为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。

  • Infrastructure层为基础实施层,向其他层提供通用的技术能力。
    DDD四层架构图

  • 基础设施层:基本上都是需要固化的代码,一次写成,需要变动的次数很少,一旦变动,就需要大量谨慎的回归测试。将所有的存储调用、中间件调用都沉淀在这一层中。

  • 防腐层:对于一些基础的接口有二次包装或者不同的需求,就以XXXManager进行封装成防腐层,对于一些二方微服务系统的调用,也建议设置成一个防腐层XXXServiceManager进行防腐管理,这样也就可以灵活的在领域层进行二方领域的简单调用,避免过多外部领域逻辑的侵入了。

3.1.2 事件驱动

  • 在服务层,对下层接口的协同有编排(Orchestration)和协调(Choreography)两种方式。
    • 在编排中,服务控制器处理微服务之间的所有通信,并指导每个服务执行预期的功能。
    • 在协调中,通过事件驱动的方式来协调微服务和函数。
  • 事件驱动的三种模式
    • 事件通知:系统发送事件消息以通知其他系统其域中的更改时;
    • 事件推送:想要更新系统的客户端时;
    • 事件溯源:对系统状态进行更改时,将该状态更改记录为一个事件,并且在未来的任何时间重新处理这些事件,从而自信地重建系统状态。

事件驱动架构图

3.1.3 CQRS

  • CQRS(Command and Query Responsibility Segregation)命令查询职责分离模式,分别对读和写建模。
  • CQRS从定义上要求:
    • 一个方法修改了对象的状态,该方法便是一个Command,它不应该返回数据。
    • 一个方法返回了数据,该方法便是一个Query,此时它不应该通过直接的或间接的手段修改对象的状态。
  • CQRS 适用于场景
    • 极少数复杂的业务领域,如果不是很适合反而会增加复杂度
    • 为获取高性能的服务
    • 单独设计每个投影,并使用它们自己的数据存储,而不要多个投影共用一个巨大的数据存储,互相影响
      CQRS架构图

3.1.4 六边形架构

  • 六边形架构(Hexagonal Architecture)又称为端口和适配器风格,为了突显这是个扁平的架构,每个边界的权重是相等的。
  • 经典分层架构分为三层(展现层、应用层、数据访问层),而对于六边形架构,可以分成另外的三层:
    • 领域层(Domain Layer):最里面,纯粹的核心业务逻辑,一般不包含任何技术实现或引用。
    • 端口层(Ports Layer):领域层之外,负责接收与用例相关的所有请求,这些请求负责在领域层中协调工作。端口层在端口内部作为领域层的边界,在端口外部则扮演了外部实体的角色。
    • 适配器层(Adapters Layer):端口层之外,负责以某种格式接收输入、及产生输出。比如,对于 HTTP 用户请求,适配器会将转换为对领域层的调用,并将领域层传回的响应进行封送,通过 HTTP 传回调用客户端。
  • 六边形架构的优点:使业务边界更加清晰,从而获得更好的扩展性,业务复杂度和技术复杂度分离也是 DDD 的重要基础。
    六边形架构图

3.2 DDD设计

3.2.1 理解业务

  • 领域驱动设计是对业务模型在系统设计中的一种表现形式,在进行DDD实战前一定要熟悉业务,不熟悉业务无法把业务模型翻译成域模型。理解业务-站在业务方和产品角度,梳理系统业务的所有细节,明白每一个业务细节点。
  • 领域模型的特点
    • 领域模型是现实世界中对象的可视化表示;
    • 领域模型是分析需求阶段的产物,是表述问题空间的模型,用于指导后续的系统设计;
    • 在设计领域模型时不应该考虑后续具体的系统实现;
  • 领域建模的意义
    • 通过建立领域模型的过程不断拉齐团队内成员对需求的认知
    • 随着真实需求的不断清晰,再反过来持续校准领域模型,使得领域模型能更好的反映真实世界
    • 领域模型建立过程中,团队成员会在有界的上下文中有意识的形成统一语言,便于后续的沟通
  • 我们裂变业务现状存在的两个问题
    • 业务分散在各处,独立维护,业务概念和模型互相独立五花八门,项目组人员对这些业务的流程并不是那么熟悉
    • 我们裂变业务现在仅仅只为单业务服务,模型大都缺乏前瞻性和扩展性,仅对自身裂变业务的模型做简单抽象得到的模型,并不能满足裂变玩法平台化的要求
  • 针对这两个问题的应对方案:重新对裂变玩法进行领域建模,目标是产出一套新的统一语言以及一套足够稳健的领域模型,能够有效的支持现有业务及未来出现的类似的新业务
  • 裂变玩法领域建模的意义
    • 通过建立裂变玩法问题域领域模型的过程不断拉齐团队内成员对这块业务的认知
    • 通过建模的过程不断挖掘准入裂变玩法的真实需求,持续校准领域模型,使得领域模型能更好的反映真实世界,也具备一定的前瞻性和扩展性

3.2.2 业务抽象

  • 在梳理业务过程中会把业务每一个具体的点都给罗列出来,是一盘零碎的业务点。通过对业务的理解进行抽象,把相关性的业务点进行分组聚合。业务抽象过程中涉及边界划分问题
  • 在建模过程中增加业务方,运营,客服等非技术性的人员的参与度,他们往往能补充技术人员对业务的盲点

3.2.3 模型翻译

  • 在经过业务抽象之后,整体业务模型已经清晰明了,在域模型设计上,只需要把业务模型经过简单翻译映射成域模型即可。

3.2.4 子域划分

  • 业务模型翻译成域模型后,当一个域模型比较复杂的时候需要把一个域模型进行子域划分

3.2.5 其它

  • Entity与持久层的DO异构
    • 贫血模型下,VO与DO的区别是很小的,基本是简单的值对应关系。通常我们使用各种copyProperties()、convertDOToVO()等操作就可以完成转换
    • 充血模型下,entity都是深层次嵌套对象,对于持久层DO转换Entity和Entity转换DO都是需要额外编写工作量不小的转换代码的。推荐使用开源工具库 MapStruct,本质是一个转换代码生成工具,提供编译时生成转换代码。

3.3 设计规范

3.3.1 Entity设计规范

  • Entity最重要的设计原则是保证实体的不变性(Invariants),也就是说要确保无论外部怎么操作,一个实体内部的属性都不能出现相互冲突,状态不一致的情况。
  • 原则1:创建即一致
    • constructor参数要包含所有必要属性,或者在constructor里有合理的默认值。
    • 使用Factory模式来降低调用方复杂度
  • 原则2:尽量避免public setter
    • 因为set单一参数会导致状态不一致的情况;
    • @ Setter(AccessLevel.PRIVATE) // 确保不生成public setter
  • 原则3:通过聚合根保证主子实体的一致性,主实体会包含子实体,主实体就起到聚合根的作用,即:
    • 子实体不能单独存在,只能通过聚合根的方法获取到。任何外部的对象都不能直接保留子实体的引用
    • 子实体没有独立的Repository,不可以单独保存和取出,必须要通过聚合根的Repository实例化
    • 子实体可以单独修改自身状态,但是多个子实体之间的状态一致性需要聚合根来保障
  • 原则4:不可以强依赖其他聚合根实体或领域服务。对外部对象的依赖性会直接导致实体无法被单测;以及一个实体无法保证外部实体变更后不会影响本实体的一致性和正确性。正确的对外部依赖的方法有两种:
    • 只保存外部实体的ID:强烈建议使用强类型的ID对象,而不是Long型ID。强类型的ID对象不单单能自我包含验证代码,保证ID值的正确性,同时还能确保各种入参不会因为参数顺序变化而出bug。
    • 针对于“无副作用”的外部依赖,通过方法入参的方式传入
  • 原则5:任何实体的行为只能直接影响到本实体(和其子实体)
  • 原则6:实体的充血模型不包含持久化逻辑

3.3.2 Domain Service 设计规范

  • 领域服务一般分为单对象策略型、跨对象事务型、通用组件型三种。
  • 单对象策略型:主要面向的是单个实体对象的变更,但涉及到多个领域对象或外部依赖的一些规则。实体应该通过方法入参的方式传入这种领域服务,然后通过Double Dispatch来反转调用领域服务的方法。
  • 跨对象事务型:当一个行为会直接修改多个实体时,不能再通过单一实体的方法作处理,而必须直接使用领域服务的方法来做操作。
  • 通用组件型:与ECS里的System类似,提供了组件化的行为,但本身又不直接绑死在一种实体类上。
  • 让Domain Service与Repository打交道,而不是让领域模型Entity与Repository打交道,因为我们想保持领域模型的独立性,不与任何其他层的代码(Repository)或开发框架(比如Spring、MyBatis)耦合在一起,将流程性的代码逻辑(比如从DB中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加可复用。
  • Domain Service类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的RPC接口等,都可以放到Domain Service类中。

3.3.3 Application Service 设计规范

  • Application Service 是业务流程的封装,不处理业务逻辑,即不要有if/else分支逻辑、不要有任何计算、一些数据的转化可以交给其他对象来做。
  • 常用的ApplicationService“套路”:
    • 准备数据:包括从外部服务或持久化源取出相对应的Entity、VO以及外部服务返回的DTO。
    • 执行操作:包括新对象的创建、赋值,以及调用领域对象的方法对其进行操作。需要注意的是这个时候通常都是纯内存操作,非持久化。
    • 持久化:将操作结果持久化,或操作外部系统产生相应的影响,包括发消息等异步操作。

3.3.4 其它规范

  • Interface层:
    • 职责:主要负责承接网络协议的转化、Session管理等
    • 接口数量:避免所谓的统一API,不必人为限制接口类的数量,每个/每类业务对应一套接口即可,接口参数应该符合业务需求,避免大而全的入参
    • 接口出参:统一返回Result
    • 异常处理:应该捕捉所有异常,避免异常信息的泄漏。可以通过AOP统一处理,避免代码里有大量重复代码。
  • Application层:
    • 入参:具像化Command、Query、Event对象作为ApplicationService的入参,唯一可以的例外是单ID查询的场景。
    • CQE的语意化:CQE对象有语意,不同用例之间语意不同,即使参数一样也要避免复用。
    • 入参校验:基础校验通过Bean Validation api解决。Spring Validation自带Validation的AOP,也可以自己写AOP。
    • 出参:统一返回DTO,而不是Entity或DO。
    • DTO转化:用DTO Assembler负责Entity/VO到DTO的转化。
    • 异常处理:不统一捕捉异常,可以随意抛异常。
  • 部分Infra层:
    • 用ACL防腐层将外部依赖转化为内部代码,隔离外部的影响

3.4 领域事件

  • 一般的副作用发生在核心领域模型状态变更后,同步或者异步对另一个对象的影响或行为。副作用的处理方法—领域事件

3.4.1 领域事件介绍

  • 领域事件是一个在领域里发生了某些事后,希望领域里其他对象能够感知到的通知机制。
  • 领域事件将隐性的副作用“显性化”,通过一个显性的事件,将事件触发和事件处理解耦,最终起到代码更清晰、扩展性更好的目的。

3.4.2 领域事件实现

  • 领域事件通常是立即执行的、在同一个进程内、可能是同步或异步。可通过一个EventBus来实现进程内的通知机制。
  • 缺陷:领域事件的很好的实施依赖EventBus、Dispatcher、Invoker这些属于框架级别的支持。但因为Entity不能直接依赖外部对象,所以EventBus目前只能是一个全局的Singleton,导致Entity对象无法被完整单测覆盖全。

3.4.3 领域建模

  • 从认识的角度来理解建模建模
  • 保证领域模型与需求严格同步:
    • 一个简单的规则:任何在需求描述中出现的概念,都必须出现的领域模型中,如果需求描述中存在概念之间的关系,领域模型中也必须有这个关系。

参考

  1. 《实现领域驱动设计》- Vaughn Vernon
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐