摘要

Eric Evans的“Domain-Driven Design领域驱动设计”简称 DDD,DDD 是一套综合软件系统分析和设计的面向对象建模方法,相比于面向对象设计(OO),DDD 是一种更加注重业务边界的设计方法,OO 更加注重抽象,从差异中寻找共同点,然后进行抽象,这是两种不同的思维方式。可以简单的理解为 DDD 是一种业务+解耦的设计实现,DDD 的业务边界思维能够很好的支持微服务拆分,与近些年提出的微服务理论不谋而合 。本文将描述领域驱动设计(DDD)的基础知识,以及 DDD 项目的基础项目结构。

一、核心思想

​ 技术主动理解业务,让不怎么理解开发技术的领域专家参与软件建设,根据领域专家提供的业务需求进行软件模块结构划分。

​ 随着产品的不断迭代和演化,业务逻辑变得越来越复杂,各个模块相互耦合将导致模块的具体功能和意图逐渐模糊,需要修改功能时将难以追溯功能需要修改的点。DDD 为了是降低业务之间的耦合度,以减少程序复杂度,提升系统更新迭代的效率。

用软件体现业务,在代码中体现领域思想,开发人员与领域专家之间进行沟通使软件质量得到改善。当团队对领域有了新理解后,领域模型也随之深化。

二、MVC缺陷

MVC 分层模型在 J2EE 中广泛运用,但其也存在如下缺陷。

  1. 是一种面向过程的编码,业务需要怎么组织,服务层中就怎么写逻辑,是一种事务脚本的逻辑;
  2. 违反单一职责原则,所有与组件的交互都在服务层中进行,逻辑复杂,与业务无关的改变会影响服务层中的业务代码;
  3. 违反开放封闭原则(对扩展开放、对修改封闭),服务层中的代码依赖太多的组件,业务功能需要扩展时需要修改核心代码。
  4. 违反依赖反转原则(面向接口依赖,而不要依赖实现类),Controller 虽然是依赖 service 接口,但是 service 内部的代码基本没有体现面向接口编程。
  5. service 中依赖的组件太多,无法单独进行测试,可测试性差。
  6. 业务逻辑与数据存储相互依赖,基本无法复用,可扩展性差。

三、DDD优势与缺陷

优势:

  1. 让代码的作用更加明确,降低项目迭代难度;
  2. 业务隔离,系统迭代时减少影响范围,降低测试难度;
  3. 方便系统进行升级和改动。

缺陷:

DDD 模式弥补了以上所述的 MVC 模式的缺陷,但是也将增加大量的类。当业务足够简单并且没有频繁改动时,没有使用领域驱动进行设计的必要,采用 MVC 分层架构进行快速开发也是不错的选择。

四、失血、贫血、充血和胀血模型

失血模型:实体类中仅包含get、set方法,所有业务逻辑完全由 service 层完成,没有 dao,service直接操作数据库。

贫血模型:除了包含get、set方法外,还包含原子服务。

充血模型:大部分业务和实体放在一起,service只有少量业务,一个实体根据业务划分为多个实体,每个实体有自己相应的业务实现。

胀血模型:取消了 service 层,在实体中封装业务。

失血模型和贫血模型实体中不包括业务,实体无法体现出业务。一般 MVC 架构采用贫血模型,DDD 模式采用充血模型。

五、DDD基础概念

5.1 值对象

​ 当一个对象用于对事物进行描述而没有唯一标识时,那么它被称作值对象(Value Object)。因为在领域中并不是任何时候一个事物都需要有一个唯一的标识,也就是说我们并不关心具体是哪个事物,只关心这个事物是什么。

5.2 领域服务

​ 一些重要的领域行为或操作,它们不太适合建模为实体对象或者值对象,它们本质上只是一些操作,并不是具体的事物,另一方面这些操作往往又会涉及到多个领域对象的操作,它们只负责来协调这些领域对象完成操作而已,那么我们可以归类它们为领域服务。

5.3 防腐层

​ 一个上下文通过一些适配和转换与另一个上下文交互,这些适配称为防腐层(Anti-corruption layer)。在不共享相同领域模型的不同子系统之间实施防腐层,此层转换一个子系统向另一个子系统发出的请求。 使用反腐层可确保应用程序的设计不受限于对外部子系统的依赖。

六、领域模型设计步骤

  1. 根据需求建立一个初步的领域模型,识别出一些明显的领域概念以及它们的关联,关联可以暂时没有方向但需要有(1:1,1:n,m:n)这些关系;可以用文字精确的没有歧义的描述出每个领域概念的涵义以及包含的主要信息;

  2. 分析主要的软件应用程序功能,识别出主要的应用层的类;这样有助于及早发现哪些是应用层的职责,哪些是领域层的职责;

  3. 进一步分析领域模型,识别出哪些是实体,哪些是值对象,哪些是领域服务;

  4. 分析关联,通过对业务的更深入分析以及各种软件设计原则及性能方面的权衡,明确关联的方向或者去掉一些不需要的关联;

  5. 找出聚合边界及聚合根,这是一件很有难度的事情;因为你在分析的过程中往往会碰到很多模棱两可的难以清晰判断的选择问题,所以,需要我们平时一些分析经验的积累才能找出正确的聚合根;

  6. 为聚合根配备仓储,一般情况下是为一个聚合分配一个仓储,此时只要设计好仓储的接口即可;

  7. 走查场景,确定我们设计的领域模型能够有效地解决业务需求;

  8. 考虑如何创建领域实体或值对象,是通过工厂还是直接通过构造函数;

  9. 停下来重构模型。寻找模型中觉得有些疑问或者是蹩脚的地方,比如思考一些对象应该通过关联导航得到还是应该从仓储获取?聚合设计的是否正确?考虑模型的性能怎样,等等;

领域建模是一个不断重构,持续完善模型的过程,大家会在讨论中将变化的部分反映到模型中,从而是模型不断细化并朝正确的方向走。

七、项目结构

基础的 DDD 项目结构示例:

- interfaces:表现层,提供给外部用户访问
    - facade:提供粗粒度的业务实现接口,将用户请求分解为多个业务操作
        - rest:REST API
        - grpc:gRPC API
- application:应用层
    - dto:存放dto数据传输对象
        - request
        - response
    - assembler:进行DTO对象和领域对象的相互转换和数据交换
    - service:对领域服务或者外部应用服务进行封装、编排和组合,对外提供粗粒度服务
- domain:领域层
    - aggregate:各个领域聚合目录,按业务名称命名,如支付功能、用户管理
        - entity:领域对象
        - valueobject:值对象
        - event:领域事件
        - service:领域服务,进行领域业务实现
        - repository:领域仓储,进行领域数据的查询和持久化
- infrastructure:基础层
    - config:存放配置相关代码
    - client:存放跨服务接口
    - common:存放消息、数据库、缓存、文件、总线、网关、公用的常量、枚举等
        - enums:存放枚举
        - cache:缓存相关功能
    - util:存放开发框架、第三方类库、通用算法等基础代码