ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

肝了一个月的 DDD,一文带你掌握!

肝了一个月的 DDD,一文带你掌握! 1. 走进 DDD1.1 为什么要用 DDD 面向对象设计数据行为绑定告别贫血模型降低复杂度分而治之优先考虑领域模型而不是切割数据和行为准确传达业务规则业务优先代码即设计它通过边界划分将复杂业务领域简单化帮我们设计出清晰的领域和应用边界可以很容易地实现业务和技术统一的架构演进领域知识共享提升协助效率增加可维护性和可读性延长软件生命周期中台化的基石。1.2 DDD 作用说到 DDD绕不开 MVC在 MVC 三层架构中我们进行功能开发的之前拿到需求解读需求。往往最先做的一步就是先设计表结构在逐层设计上层 daoservicecontroller。对于产品或者用户的需求都做了一层自我理解的转化。用户需求在被提出之后经过这么多层的转化后特别是研发需求在数据库结构这一层转化后将业务以主观臆断行为进行了转化。一旦业务边界划分模糊考虑不全大量的逻辑补充堆积到了代码层实现变得越来越难维护。假如我们现在要做一个电商订单下单的需求涉及到用户选定商品下订单、支付订单、对用户下单时的订单发货MVC 架构我们常见的做法是在分析好业务需求之后就开始设计表结构了订单表支付表商品表等等。然后编写业务逻辑。这是第一个版本的需求功能迭代饿了订单支付后我可以取消下单的商品我们退换货是不是又需要进行加表紧跟着对于的实现逻辑也进行修改。功能不断迭代代码就不断的层层往上叠。DDD 架构我们先进行划分业务边界。这里面核心是订单。那么订单就是这个业务领域里面的聚合逻辑体现。支付商品信息地址等等都是围绕着订单实体。订单本身的属性决定之后类似于地址只是一个属性的体现。当你将订单的领域模型构建好之后后续的逻辑边界与仓储设计也就随之而来了。DDD 整体作用总结如下消除信息不对称常规MVC三层架构中自底向上的设计方式做一个反转以业务为主导自顶向下的进行业务领域划分将大的业务需求进行拆分分而治之。2. DDD 架构2.1 DDD 分层架构严格分层架构某层只能与直接位于的下层发生耦合。松散分层架构允许上层与任意下层发生耦合。在领域驱动设计DDD中采用的是松散分层架构层间关系不那么严格。每层都可能使用它下面所有层的服务而不仅仅是下一层的服务。每层都可能是半透明的这意味着有些服务只对上一层可见而有些服务对上面的所有层都可见。分层的作用从上往下用户交互层web 请求rpc 请求mq 消息等外部输入均被视为外部输入的请求可能修改到内部的业务数据。业务应用层与 MVC 中的 service 不同的不是service 中存储着大量业务逻辑。但在应用服务的实现中它负责编排、转发、校验等。领域层或称为模型层系统的核心负责表达业务概念业务状态信息以及业务规则。即包含了该领域所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上可以从实体值对象聚合聚合根领域服务领域事件仓储工厂等方面入手。基础设施层主要有 2 方面内容一是为领域模型提供持久化机制当软件需要持久化能力时候才需要进行规划一是对其他层提供通用的技术支持能力如消息通信通用工具配置等的实现。在设计和开发时不要将本该放在领域层的业务逻辑放到应用层中实现因为庞大的应用层会使领域模型失焦时间一长你的服务就会演化为传统的三层架构业务逻辑会变得混乱。2.2 各层数据转换每一层都有自己特定的数据可以做如下区分VOView Object视图对象主要对应界面显示的数据对象。对于一个WEB页面或者SWT、SWING的一个界面用一个VO对象对应整个界面的值。DTOData Transfer Object数据传输对象主要用于远程调用等需要大量传输对象的地方。比如我们一张表有 100 个字段那么对应的 PO 就有 100 个属性。但是我们界面上只要显示 10 个字段客户端用 WEB service 来获取数据没有必要把整个 PO 对象传递到客户端这时我们就可以用只有这 10 个属性的 DTO 来传递结果到客户端这样也不会暴露服务端表结构。到达客户端以后如果用这个对象来对应界面显示那此时它的身份就转为 VO。在这里我泛指用于展示层与服务层之间的数据传输对象。DODomain Object领域对象就是从现实世界中抽象出来的有形或无形的业务实体。POPersistent Object持久化对象它跟持久层通常是关系型数据库的数据结构形成一一对应的映射关系如果持久层是关系型数据库那么数据表中的每个字段或若干个就对应 PO 的一个或若干个属性。最形象的理解就是一个 PO 就是数据库中的一条记录好处是可以把一条记录作为一个对象处理可以方便的转为其它对象。3. DDD 基础学习 DDD 前有很多基础概念需要掌握这幅图总结的很全他把 DDD 划分不同的层级最里层是值、属性、唯一标识等这个是最基本的数据单位但不能直接使用。然后是实体这个把基础的数据进行封装可以直接使用在代码中就是封装好的一个个实体对象。之后就是领域层它按照业务划分为不同的领域比如订单领域、商品领域、支付领域等。最后是应用服务它对业务逻辑进行编排也可以理解为业务层。3.1 领域和子域在研究和解决业务问题时DDD 会按照一定的规则将业务领域进行细分当领域细分到一定的程度后DDD 会将问题范围限定在特定的边界内在这个边界内建立领域模型进而用代码实现该领域模型解决相应的业务问题。简言之DDD 的领域就是这个边界内要解决的业务问题域。领域可以进一步划分为子领域。我们把划分出来的多个子领域称为子域每个子域对应一个更小的问题域或更小的业务范围。领域的核心思想就是将问题域逐级细分来降低业务理解和系统实现的复杂度。通过领域细分逐步缩小服务需要解决的问题域构建合适的领域模型。举个简单的例子对于保险领域我们可以把保险细分为承保、收付、再保以及理赔等子域而承保子域还可以继续细分为投保、保全寿险、批改财险等子子域。3.2 核心域、通用域和支撑域子域可以根据重要程度和功能属性划分为如下核心域决定产品和公司核心竞争力的子域它是业务成功的主要因素和公司的核心竞争力。通用域没有太多个性化的诉求同时被多个子域使用的通用功能的子域。支撑域但既不包含决定产品和公司核心竞争力的功能也不包含通用功能的子域。核心域、支撑域和通用域的主要目标通过领域划分区分不同子域在公司内的不同功能属性和重要性从而公司可对不同子域采取不同的资源投入和建设策略其关注度也会不一样。很多公司的业务表面看上去相似但商业模式和战略方向是存在很大差异的因此公司的关注点会不一样在划分核心域、通用域和支撑域时其结果也会出现非常大的差异。比如同样都是电商平台的淘宝、天猫、京东和苏宁易购他们的商业模式是不同的。淘宝是 C2C 网站个人卖家对个人买家而天猫、京东和苏宁易购则是 B2C 网站是公司卖家对个人买家。即便是苏宁易购与京东都是 B2C 的模式苏宁易购是典型的传统线下卖场转型成为电商京东则是直营加部分平台模式。因此在公司建立领域模型时我们就要结合公司战略重点和商业模式重点关注核心域。3.3 通用语言和限界上下文通用语言就是能够简单、清晰、准确描述业务涵义和规则的语言。限界上下文用来封装通用语言和领域对象提供上下文环境保证在领域之内的一些术语、业务相关对象等通用语言有一个确切的含义没有二义性。3.3.1 通用语言通用语言是团队统一的语言不管你在团队中承担什么角色在同一个领域的软件生命周期里都使用统一的语言进行交流。那么通用语言的价值也就很明了它可以解决交流障碍这个问题使领域专家和开发人员能够协同合作从而确保业务需求的正确表达。这个通用语言到场景落地大家可能还很模糊其实就是把领域对象、属性、代码模型对象等通过代码和文字建立映射关系可以通过 Excel 记录这个关系这样研发可以通过代码知道这个含义产品或者业务方可以通过文字知道这个含义沟通起来就不会有歧义说的简单一点其实就是统一产品和研发的话术。直接看下面这幅图来源于极客时间欧创新的 DDD 实战课3.3.2 限界上下文通用语言也有它的上下文环境为了避免同样的概念或语义在不同的上下文环境中产生歧义DDD 在战略设计上提出了“限界上下文”这个概念用来确定语义所在的领域边界。限界上下文是一个显式的语义和语境上的边界领域模型便存在于边界之内。边界内通用语言中的所有术语和词组都有特定的含义。把限界上下文拆解开看限界就是领域的边界而上下文则是语义环境。通过领域的限界上下文我们就可以在统一的领域边界内用统一的语言进行交流。3.4 实体和值对象3.4.1 实体实体 唯一身份标识 可变性【状态 行为】DDD 中要求实体是唯一的且可持续变化的。意思是说在实体的生命周期内无论其如何变化其仍旧是同一个实体。唯一性由唯一的身份标识来决定的。可变性也正反映了实体本身的状态和行为。实体以 DO领域对象的形式存在每个实体对象都有唯一的 ID。我们可以对一个实体对象进行多次修改修改后的数据和原来的数据可能会大不相同。但是由于它们拥有相同的 ID它们依然是同一个实体。比如商品是商品上下文的一个实体通过唯一的商品 ID 来标识不管这个商品的数据如何变化商品的 ID 一直保持不变它始终是同一个商品。3.4.2 值对象值对象 将一个值用对象的方式进行表述来表达一个具体的固定不变的概念。当你只关心某个对象的属性时该对象便可作为一个值对象。 我们需要将值对象看成不变对象不要给它任何身份标识还应该尽量避免像实体对象一样的复杂性。还是举个订单的例子订单是一个实体里面包含地址这个地址可以只通过属性嵌入的方式形成的订单实体对象也可以将地址通过 json 序列化一个 string 类型的数据存到 DB 的一个字段中那么这个 Json 串就是一个值对象是不是很好理解下面给个简单的图同样是源于极客时间欧创新的 DDD 实战课3.5 聚合和聚合根3.5.1 聚合聚合我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。聚合是领域对象的显式分组旨在支持领域模型的行为和不变性同时充当一致性和事务性边界。聚合有一个聚合根和上下文边界这个边界根据业务单一职责和高内聚原则定义了聚合内部应该包含哪些实体和值对象而聚合之间的边界是松耦合的。按照这种方式设计出来的服务很自然就是“高内聚、低耦合”的。聚合在 DDD 分层架构里属于领域层领域层包含了多个聚合共同实现核心业务逻辑。跨多个实体的业务逻辑通过领域服务来实现跨多个聚合的业务逻辑通过应用服务来实现。比如有的业务场景需要同一个聚合的 A 和 B 两个实体来共同完成我们就可以将这段业务逻辑用领域服务来实现而有的业务逻辑需要聚合 C 和聚合 D 中的两个服务共同完成这时你就可以用应用服务来组合这两个服务。3.5.2 聚合根如果把聚合比作组织那聚合根就是这个组织的负责人。聚合根也称为根实体它不仅是实体还是聚合的管理者。首先它作为实体本身拥有实体的属性和业务行为实现自身的业务逻辑。其次它作为聚合的管理者在聚合内部负责协调实体和值对象按照固定的业务规则协同完成共同的业务逻辑。最后在聚合之间它还是聚合对外的接口人以聚合根 ID 关联的方式接受外部任务和请求在上下文内实现聚合之间的业务协同。也就是说聚合之间通过聚合根 ID 关联引用如果需要访问其它聚合的实体就要先访问聚合根再导航到聚合内部实体外部对象不能直接访问聚合内实体。上面讲的还是有些抽象下面看一个图就能很好理解同样是源于极客时间欧创新的DDD实战课简单概括一下通过事件风暴我理解就是头脑风暴不过我们一般都是先通过个人理解然后再和相关核心同学进行沟通得到实体和值对象将这些实体和值对象聚合为“投保聚合”和“客户聚合”其中“投保单”和“客户”是两者的聚合根找出与聚合根“投保单”和“客户”关联的所有紧密依赖的实体和值对象在聚合内根据聚合根、实体和值对象的依赖关系画出对象的引用和依赖模型。3.6 领域服务和应用服务3.6.1 领域服务当一些逻辑不属于某个实体时可以把这些逻辑单独拿出来放到领域服务中理想的情况是没有领域服务如果领域服务使用不恰当慢慢又演化回了以前逻辑都在 service 层的局面。可以使用领域服务的情况执行一个显著的业务操作对领域对象进行转换以多个领域对象作为输入参数进行计算结果产生一个值对象3.6.2 应用服务应用层作为展现层与领域层的桥梁是用来表达用例和用户故事的主要手段。应用层通过应用服务接口来暴露系统的全部功能。在应用服务的实现中它负责编排和转发它将要实现的功能委托给一个或多个领域对象来实现它本身只负责处理业务用例的执行顺序以及结果的拼装。通过这样一种方式它隐藏了领域层的复杂性及其内部实现机制。应用层相对来说是较“薄”的一层除了定义应用服务之外在该层我们可以进行安全认证权限校验持久化事务控制或者向其他系统发生基于事件的消息通知另外还可以用于创建邮件以发送给客户等。3.7 领域事件领域事件 事件发布 事件存储 事件分发 事件处理。领域事件是一个领域模型中极其重要的部分用来表示领域中发生的事件。忽略不相关的领域活动同时明确领域专家要跟踪或希望被通知的事情或与其他模型对象中的状态更改相关联。下面简单说明领域事件事件发布构建一个事件需要唯一标识然后发布事件存储发布事件前需要存储因为接收后的事建也会存储可用于重试或对账等事件分发服务内直接发布给订阅者服务外需要借助消息中间件比如KafkaRabbitMQ等事件处理先将事件存储然后再处理。比如下订单后给用户增长积分与赠送优惠券的需求。如果使用瀑布流的方式写代码。一个个逻辑调用那么不同用户赠送的东西不同逻辑就会变得又臭又长。这里的比较好的方式是用户下订单成功后发布领域事件积分聚合与优惠券聚合监听订单发布的领域事件进行处理。3.8 资源库【仓储】仓储介于领域模型和数据模型之间主要用于聚合的持久化和检索。它隔离了领域模型和数据模型以便我们关注于领域模型而不需要考虑如何进行持久化。我们将暂时不使用的领域对象从内存中持久化存储到磁盘中。当日后需要再次使用这个领域对象时根据 key 值到数据库查找到这条记录然后将其恢复成领域对象应用程序就可以继续使用它了这就是领域对象持久化存储的设计思想。是不是感觉这块内容比较抽象直接对着Demo学习吧很多东西你就会豁然开朗。4. DDD实战4.1 项目介绍主要是围绕用户、角色和两者的关系构建权限分配领域模型。采用 DDD 4 层架构包括用户接口层、应用层、领域层和基础服务层。数据通过 VO、DTO、DO、PO 转换进行分层隔离。采用 SpringBoot MyBatis Plus 框架存储用 MySQL。4.2 工程目录项目划分为用户接口层、应用层、领域层和基础服务层每一层的代码结构都非常清晰包括每一层 VO、DTO、DO、PO 的数据定义对于每一层的公共代码比如常量、接口等都抽离到 ddd-common 中。./ddd-application // 应用层 ├── pom.xml └── src └── main └── java └── com └── ddd └── applicaiton ├── converter │ └── UserApplicationConverter.java // 类型转换器 └── impl └── AuthrizeApplicationServiceImpl.java // 业务逻辑 ./ddd-common ├── ddd-common // 通用类库 │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── common │ ├── exception // 异常 │ │ ├── ServiceException.java │ │ └── ValidationException.java │ ├── result // 返回结果集 │ │ ├── BaseResult.javar │ │ ├── Page.java │ │ ├── PageResult.java │ │ └── Result.java │ └── util // 通用工具 │ ├── GsonUtil.java │ └── ValidationUtil.java ├── ddd-common-application // 业务层通用模块 │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── applicaiton │ ├── dto // DTO │ │ ├── RoleInfoDTO.java │ │ └── UserRoleDTO.java │ └── servic // 业务接口 │ └── AuthrizeApplicationService.java ├── ddd-common-domain │ ├── pom.xml │ └── src │ └── main │ └── java │ └── com │ └── ddd │ └── domain │ ├── event // 领域事件 │ │ ├── BaseDomainEvent.java │ │ └── DomainEventPublisher.java │ └── service // 领域接口 │ └── AuthorizeDomainService.java └── ddd-common-infra ├── pom.xml └── src └── main └── java └── com └── ddd └── infra ├── domain // DO │ └── AuthorizeDO.java ├── dto │ ├── AddressDTO.java │ ├── RoleDTO.java │ ├── UnitDTO.java │ └── UserRoleDTO.java └── repository ├── UserRepository.java // 领域仓库 └── mybatis └── entity // PO ├── BaseUuidEntity.java ├── RolePO.java ├── UserPO.java └── UserRolePO.java ./ddd-domian // 领域层 ├── pom.xml └── src └── main └── java └── com └── ddd └── domain ├── event // 领域事件 │ ├── DomainEventPublisherImpl.java │ ├── UserCreateEvent.java │ ├── UserDeleteEvent.java │ └── UserUpdateEvent.java └── impl // 领域逻辑 └── AuthorizeDomainServiceImpl.java ./ddd-infra // 基础服务层 ├── pom.xml └── src └── main └── java └── com └── ddd └── infra ├── config │ └── InfraCoreConfig.java // 扫描Mapper文件 └── repository ├── converter │ └── UserConverter.java // 类型转换器 ├── impl │ └── UserRepositoryImpl.java └── mapper ├── RoleMapper.java ├── UserMapper.java └── UserRoleMapper.java ./ddd-interface ├── ddd-api // 用户接口层 │ ├── pom.xml │ └── src │ └── main │ ├── java │ │ └── com │ │ └── ddd │ │ └── api │ │ ├── DDDFrameworkApiApplication.java // 启动入口 │ │ ├── converter │ │ │ └── AuthorizeConverter.java // 类型转换器 │ │ ├── model │ │ │ ├── req // 入参 req │ │ │ │ ├── AuthorizeCreateReq.java │ │ │ │ └── AuthorizeUpdateReq.java │ │ │ └── vo // 输出 VO │ │ │ └── UserAuthorizeVO.java │ │ └── web // API │ │ └── AuthorizeController.java │ └── resources // 系统配置 │ ├── application.yml │ └── resources // Sql文件 │ └── init.sql └── ddd-task └── pom.xml ./pom.xml4.3 数据库包括 3 张表分别为用户、角色和用户角色表一个用户可以拥有多个角色一个角色可以分配给多个用户。create table t_user ( id bigint auto_increment comment 主键 primary key, user_name varchar(64) null comment 用户名, password varchar(255) null comment 密码, real_name varchar(64) null comment 真实姓名, phone bigint null comment 手机号, province varchar(64) null comment 用户名, city varchar(64) null comment 用户名, county varchar(64) null comment 用户名, unit_id bigint null comment 单位id, unit_name varchar(64) null comment 单位名称, gmt_create datetime default CURRENT_TIMESTAMP not null comment 创建时间, gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment 修改时间, deleted bigint default 0 not null comment 是否删除非0为已删除 )comment 用户表 collate utf8_bin; create table t_role ( id bigint auto_increment comment 主键 primary key, name varchar(256) not null comment 名称, code varchar(64) null comment 角色code, gmt_create datetime default CURRENT_TIMESTAMP not null comment 创建时间, gmt_modified datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment 修改时间, deleted bigint default 0 not null comment 是否已删除 )comment 角色表 charset utf8; create table t_user_role ( id bigint auto_increment comment 主键id primary key, user_id bigint not null comment 用户id, role_id bigint not null comment 角色id, gmt_create datetime default CURRENT_TIMESTAMP not null comment 创建时间, gmt_modified datetime default CURRENT_TIMESTAMP not null comment 修改时间, deleted bigint default 0 not null comment 是否已删除 )comment 用户角色关联表 charset utf8;4.4 基础服务层仓储资源库介于领域模型和数据模型之间主要用于聚合的持久化和检索。它隔离了领域模型和数据模型以便我们关注于领域模型而不需要考虑如何进行持久化。比如保存用户需要将用户和角色一起保存也就是创建用户的同时需要新建用户的角色权限这个可以直接全部放到仓储中public AuthorizeDO save(AuthorizeDO user) { UserPO userPo userConverter.toUserPo(user); if(Objects.isNull(user.getUserId())){ userMapper.insert(userPo); user.setUserId(userPo.getId()); } else { userMapper.updateById(userPo); userRoleMapper.delete(Wrappers.UserRolePOlambdaQuery() .eq(UserRolePO::getUserId, user.getUserId())); } ListUserRolePO userRolePos userConverter.toUserRolePo(user); userRolePos.forEach(userRoleMapper::insert); return this.query(user.getUserId()); }仓储对外暴露的接口如下// 用户领域仓储 public interface UserRepository { // 删除 void delete(Long userId); // 查询 AuthorizeDO query(Long userId); // 保存 AuthorizeDO save(AuthorizeDO user); }基础服务层不仅仅包括资源库与第三方的调用都需要放到该层Demo 中没有该示例我们可以看一个小米内部具体的实际项目他把第三方的调用放到了 remote 目录中4.5 领域层4.5.1 聚合聚合根我们有用户和角色两个实体可以将用户、角色和两者关系进行聚合然后用户就是聚合根聚合之后的属性我们称之为“权限”。对于地址 Address目前是作为字段属性存储到 DB 中如果对地址无需进行检索可以把地址作为“值对象”进行存储即把地址序列化为 Json 存存储到 DB 的一个字段中。public class AuthorizeDO { // 用户ID private Long userId; // 用户名 private String userName; // 真实姓名 private String realName; // 手机号 private String phone; // 密码 private String password; // 用户单位 private UnitDTO unit; // 用户地址 private AddressDTO address; // 用户角色 private ListRoleDTO roles; }4.5.2 领域服务Demo中的领域服务比较薄通过单位ID后去获取单位名称构建单位信息Service public class AuthorizeDomainServiceImpl implements AuthorizeDomainService { Override // 设置单位信息 public void associatedUnit(AuthorizeDO authorizeDO) { String unitName 武汉小米;// TODO: 通过第三方获取 authorizeDO.getUnit().setUnitName(unitName); } }我们其实可以把领域服务再进一步抽象可以抽象出领域能力通过这些领域能力去构建应用层逻辑比如账号相关的领域能力可以包括授权领域能力、身份认证领域能力等这样每个领域能力相对独立就不会全部揉到一个文件中下面是实际项目的领域层截图4.5.3 领域事件领域事件 事件发布 事件存储 事件分发 事件处理。这个 Demo 中对领域事件的处理非常简单还是一个应用内部的领域事件就是每次执行一次具体的操作时把行为记录下来。Demo 中没有记录事件的库表事件的分发还是同步的方式所以 Demo 中的领域事件还不完善后面我会再继续完善 Demo 中的领域事件通过 Java 消息机制实现解耦甚至可以借助消息队列实现异步。/** * 领域事件基类 * * author louzai * since 2021/11/22 */ Getter Setter NoArgsConstructor public abstract class BaseDomainEventT implements Serializable { private static final long serialVersionUID 1465328245048581896L; /** * 发生时间 */ private LocalDateTime occurredOn; /** * 领域事件数据 */ private T data; public BaseDomainEvent(T data) { this.data data; this.occurredOn LocalDateTime.now(); } } /** * 用户新增领域事件 * * author louzai * since 2021/11/20 */ public class UserCreateEvent extends BaseDomainEventAuthorizeDO { public UserCreateEvent(AuthorizeDO user) { super(user); } }/** * 领域事件发布实现类 * * author louzai * since 2021/11/20 */ Component Slf4j public class DomainEventPublisherImpl implements DomainEventPublisher { Autowired private ApplicationEventPublisher applicationEventPublisher; Override public void publishEvent(BaseDomainEvent event) { log.debug(发布事件,event:{}, GsonUtil.gsonToString(event)); applicationEventPublisher.publishEvent(event); } }4.4 应用层应用层就非常好理解了只负责简单的逻辑编排比如创建用户授权Transactional(rollbackFor Exception.class) public void createUserAuthorize(UserRoleDTO userRoleDTO){ // DTO转为DO AuthorizeDO authorizeDO userApplicationConverter.toAuthorizeDo(userRoleDTO); // 关联单位单位信息 authorizeDomainService.associatedUnit(authorizeDO); // 存储用户 AuthorizeDO saveAuthorizeDO userRepository.save(authorizeDO); // 发布用户新建的领域事件 domainEventPublisher.publishEvent(new UserCreateEvent(saveAuthorizeDO)); }查询用户授权信息Override public UserRoleDTO queryUserAuthorize(Long userId) { // 查询用户授权领域数据 AuthorizeDO authorizeDO userRepository.query(userId); if (Objects.isNull(authorizeDO)) { throw ValidationException.of(UserId is not exist., null); } // DO转DTO return userApplicationConverter.toAuthorizeDTO(authorizeDO); }细心的同学可以发现我们应用层和领域层通过 DTO 和 DO 进行数据转换。4.5 用户接口层最后就是提供 API 接口GetMapping(/query) public ResultUserAuthorizeVO query(RequestParam(userId) Long userId){ UserRoleDTO userRoleDTO authrizeApplicationService.queryUserAuthorize(userId); ResultUserAuthorizeVO result new Result(); result.setData(authorizeConverter.toVO(userRoleDTO)); result.setCode(BaseResult.CODE_SUCCESS); return result; } PostMapping(/save) public ResultObject create(RequestBody AuthorizeCreateReq authorizeCreateReq){ authrizeApplicationService.createUserAuthorize(authorizeConverter.toDTO(authorizeCreateReq)); return Result.ok(BaseResult.INSERT_SUCCESS); }数据的交互包括入参、DTO 和 VO都需要对数据进行转换。4.6 项目运行新建库表通过文件 ddd-interface/ddd-api/src/main/resources/init.sql 新建库表。修改 SQL 配置修改 ddd-interface/ddd-api/src/main/resources/application.yml 的数据库配置。启动服务直接启动服务即可。测试用例请求 URLhttp://127.0.0.1:8087/api/user/savePost body{userName:louzai,realName:楼,phone:13123676844,password:***,unitId:2,province:湖北省,city:鄂州市,county:葛店开发区,roles:[{roleId:2}]}4.7 项目地址DDD Demo 代码已经上传到 GitHub 中https://github.com/lml200701158/ddd-framework或者通过下面命令直接获取git clone gitgithub.com:lml200701158/ddd-framework.git5. 结语谈谈我对 DDD 的理解我觉得 DDD 不像一门技术我理解的技术比如高并发、缓存、消息队列等DDD 更像是一项软技能一种方法论包含了很多设计理念。这篇文章写于去年所以当时对 DDD 理解的其实还不够深入今年做过一些 DDD 的项目所以现在对 DDD 的理解又加深了几分。大家不要认为掌握了一些概念以及 DDD 的基本思想就掌握了 DDD然后做项目时照葫芦画瓢这样你会死的很惨只掌握 DDD 表面的东西其实是不够的我觉得 DDD 最复杂的地方其实是在它的领域设计部分项目启动前你一定要设计各个领域对象以及它们直接的交互关系。比如我们之前做过一个项目因为这块没有做好大家一边写代码一边还在思考这个领域对象该如何构造严重影响开发效率最后又不得不回退到 MVC 的模式。不要为了炫技啥都要搞个 DDD两者如何选择MVC上来就可以开干短平快前期用起来很香整体开发效率也更高所以对于紧急或者不那么重要的项目我会直接用 MVC 怼不好的地方就是后面会越来越复杂可能最后就是一坨屎山但是很多时候比如老板进度催的紧我哪想到那么多以后呢DDD前期需要花大量时间设计好领域模型对于一些基础组件或者一些核心服务如果对象模型非常复杂建议采用 DDD前期可能会稍微痛苦一些但是后期维护起来会非常方便。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进