前言
第一次接触到DDD架构,还是在小傅哥的星球里,看到了该博主的项目都是基于DDD设计的,然后就去了解了一下,DDD和MVC有哪些不同,发现DDD思想确实能够解决MVC的一些诟病,所以为此针对DDD架构进行一次较为深入的学习和实现。
问题分析
mvc其实有很多的特点,比如简单,容易,好理解,但是也正是因为其简单的设计分层逻辑,而使得在更加复杂的场景且长期维护的项目,代码迭代成本会越来越高。如图所示
在这个图中可以清晰的看到,调用错综复杂,尤其是Service 和 PO,VO之间的调用,假如说有一个较大型且已经长期维护项目的MVC架构,那么对应里面的DAO,PO,VO在Service中相互调用,那么长期开发下来,就会导致各个PO里的属性字段数量都被撑的特别特别大。这样的开发方式,使得代码的意图渐渐模糊,膨胀,臃肿且不稳定,就会让迭代成本增加。
那么DDD架构又是什么样子的呢?
DDD架构算的上是以解决MVC遗留的问题为主,其实也就是将各个属于自己领域范围内的行为和逻辑封装到自己的领域包下自己进行处理,这是DDD架构设计的精髓所在。
它希望在分治层面上合理切割问题空间为更小规模的若干子问题,而问题越小就越容易被理解和解决,能够做到高内聚和低耦合。
而在这里,我自己的理解其实是,MVC由于错综复杂的调用,导致后期的维护性很差,而使用DDD使得每个模块尽可能的都在自己的领域包中,那么就会大幅度的减少 PO VO的调用,其实也就是做到尽可能的向下调用,而不是平级调用,如果真的需要用到了平级调用,那么里应该抽取公共的领域包供大家调用。
理解
在这里还是按照小傅哥所提到的简单开发模型来理解,开发模型在MVC中可以理解为 “定义属性 - 创建方法 - 调用展示” 但是在DDD结构中,就过于简单了,在DDD中将模型抽象为“触发 - 函数 - 连接”
如上图所示,以一个微服务场景距离,那么一个系统的调用方式就不只是HTTP,还应该包括RPC远程调用,MQ消息,TASK任务,因此这些方式都可以理解为触发。通过触发去调用函数方法,这里可以把各个服务都当成一个函数方法来看,在我看来有点像各个领域包。而函数方法通过连接,调用到其他的接口,数据库,缓存来完成函数逻辑。
架构分层
直接就用小傅哥这张图来看,其实这就是一个最基本的DDD分层架构,在这里我来详细的介绍一下。
1.xfg-frame-api 接口定义
如上图所示,其实该module更多做的是为xfg-frame-trigger提供定义好的RPC接口,但是不仅仅只有这些,还有像HTTP接口,MQ接口,甚至TASK接口都可以在xfg-frame-api中定义好,因为xfg-frame-trigger引用了xfg-frame-api,所以会直接impl即可。
该module还引入了通用类型xfg-frame-types
2.xfg-frame-app 应用封装
该module在我看来是应用启动和配置这一层的,比如你可以设置一些基本的配置,如AOP配置,线程池配置,数据库连接池配置等,通过整个项目的配置文件也在此处,包括 mybatis.xml 和 application.yaml等配置。
这个类可以说是非常的重要,打包镜像就是在这一层,该module引入了xfg-frame-trigger 触发调用 和 xfg-frame-infrastructure 基础设施,也就是说将来在打包的时候,它可以被理解为专门为了启动服务而存在的。
3.xfg-frame-domain 领域封装
领域模型服务,是一个非常重要的模块,无论怎么做DDD的分层架构,domain肯定是要存在的,那么在这一层中就要一个个的细分领域模型,也就是包含每个领域模型都要有的三大要素:“模型 仓库 服务”。
4.xfg-frame-infrastructure 基础设施
这个层是依赖于domain领域层的,因为在domain层定义了仓库接口 需要在基础层实现,这是一个依赖倒置的一种设计思路。
其实我刚研究到这里的时候是懵了的,那么接下来就详细说一下,这个地方是怎么实现依赖倒置的。
首先基础层 引入了 domain层,基础层的定义其实我认为简单来说就是做 数据库 缓存等一些服务,先假设是一堆增删改查吧。那么当你基础层实现了这个接口,因为 domain领域并没有引入 基础层领域,而基础层领域引入了domain领域,所以基础层 实现了domain 仓库里面的接口。而前面提到了,其实xfg-frame-app是一个完整的打包应用,它引入了xfg-frame-trigger 触发调用 和 xfg-frame-infrastructure 基础设施,那么实际的依赖倒置其实就是这个意思:
前面说到了,在基础层实现了domain领域的接口,实现了其对应的接口的功能,而 xfg-frame-trigger中也引用了domain,那么通过多态,实际上,那么trigger中引入的domain里的仓库方法实际上由infrastructure 实现了
5.xfg-frame-trigger 触发调用
该层相对好理解,主要用于提供接口实现,消息接收,任务执行等,相当于触发器层,主要引入了 domain api 和 types
6.xfg-frame-types 类型定义
主要是作为通用类型定义层,在我们的系统开发中,会有很多类型的定义,包括基本的Response,Contants和枚举。它们会被其他层引用。
领域分层
这个地方是我人为DDD的核心部分了,也就是domain部分,前面其实还是较少介绍的,只说了其包含三个部分“模型 仓库 服务” 但是实际上包含着更多的内容,如下图所示:(转自小傅哥)
DDD 领域驱动设计的中心,主要在于领域模型的设计,以领域所需驱动功能实现和数据建模。一个领域服务下面会有多个领域模型,每个领域模型都是一个充血结构。一个领域模型 = 一个充血结构
而其中最关键的也正如前面所说的 分为了 model模型 repository仓储 service服务
- model 模型对象;
aggreate:聚合对象,实体对象、值对象的协同组织,就是聚合对象。
entity:实体对象,大多数情况下,实体对象(Entity)与数据库持久化对象(PO)是1v1的关系,但也有为了封装一些属性信息,会出现1vn的关系。
valobj:值对象,通过对象属性值来识别的对象 By 《实现领域驱动设计》
- repository 仓储服务;从数据库等数据源中获取数据,传递的对象可以是聚合对象、实体对象,返回的结果可以是;实体对象、值对象。因为仓储服务是由基础层(infrastructure) 引用领域层(domain),是一种依赖倒置的结构,但它可以天然的隔离PO数据库持久化对象被引用。
- service 服务设计;这里要注意,不要以为定义了聚合对象,就把超越1个对象以外的逻辑,都封装到聚合中,这会让你的代码后期越来越难维护。聚合更应该注重的是和本对象相关的单一简单封装场景,而把一些重核心业务放到 service 里实现。
架构源码
源码:https://gitcode.net/KnowledgePlanet/road-map/xfg-frame-ddd
. ├── README.md ├── docs │ ├── dev-ops │ │ ├── environment │ │ │ └── environment-docker-compose.yml │ │ ├── siege.sh │ │ └── skywalking │ │ └── skywalking-docker-compose.yml │ ├── doc.md │ ├── sql │ │ └── road-map.sql │ └── xfg-frame-ddd.drawio ├── pom.xml ├── xfg-frame-api │ ├── pom.xml │ ├── src │ │ └── main │ │ └── java │ │ └── cn │ │ └── bugstack │ │ └── xfg │ │ └── frame │ │ └── api │ │ ├── IAccountService.java │ │ ├── IRuleService.java │ │ ├── model │ │ │ ├── request │ │ │ │ └── DecisionMatterRequest.java │ │ │ └── response │ │ │ └── DecisionMatterResponse.java │ │ └── package-info.java │ └── xfg-frame-api.iml ├── xfg-frame-app │ ├── Dockerfile │ ├── build.sh │ ├── pom.xml │ ├── src │ │ ├── main │ │ │ ├── bin │ │ │ │ ├── start.sh │ │ │ │ └── stop.sh │ │ │ ├── java │ │ │ │ └── cn │ │ │ │ └── bugstack │ │ │ │ └── xfg │ │ │ │ └── frame │ │ │ │ ├── Application.java │ │ │ │ ├── aop │ │ │ │ │ ├── RateLimiterAop.java │ │ │ │ │ └── package-info.java │ │ │ │ └── config │ │ │ │ ├── RateLimiterAopConfig.java │ │ │ │ ├── RateLimiterAopConfigProperties.java │ │ │ │ ├── ThreadPoolConfig.java │ │ │ │ ├── ThreadPoolConfigProperties.java │ │ │ │ └── package-info.java │ │ │ └── resources │ │ │ ├── application-dev.yml │ │ │ ├── application-prod.yml │ │ │ ├── application-test.yml │ │ │ ├── application.yml │ │ │ ├── logback-spring.xml │ │ │ └── mybatis │ │ │ ├── config │ │ │ │ └── mybatis-config.xml │ │ │ └── mapper │ │ │ ├── RuleTreeNodeLine_Mapper.xml │ │ │ ├── RuleTreeNode_Mapper.xml │ │ │ └── RuleTree_Mapper.xml │ │ └── test │ │ └── java │ │ └── cn │ │ └── bugstack │ │ └── xfg │ │ └── frame │ │ └── test │ │ └── ApiTest.java │ └── xfg-frame-app.iml ├── xfg-frame-ddd.iml ├── xfg-frame-domain │ ├── pom.xml │ ├── src │ │ └── main │ │ └── java │ │ └── cn │ │ └── bugstack │ │ └── xfg │ │ └── frame │ │ └── domain │ │ ├── order │ │ │ ├── model │ │ │ │ ├── aggregates │ │ │ │ │ └── OrderAggregate.java │ │ │ │ ├── entity │ │ │ │ │ ├── OrderItemEntity.java │ │ │ │ │ └── ProductEntity.java │ │ │ │ ├── package-info.java │ │ │ │ └── valobj │ │ │ │ ├── OrderIdVO.java │ │ │ │ ├── ProductDescriptionVO.java │ │ │ │ └── ProductNameVO.java │ │ │ ├── repository │ │ │ │ ├── IOrderRepository.java │ │ │ │ └── package-info.java │ │ │ └── service │ │ │ ├── OrderService.java │ │ │ └── package-info.java │ │ ├── rule │ │ │ ├── model │ │ │ │ ├── aggregates │ │ │ │ │ └── TreeRuleAggregate.java │ │ │ │ ├── entity │ │ │ │ │ ├── DecisionMatterEntity.java │ │ │ │ │ └── EngineResultEntity.java │ │ │ │ ├── package-info.java │ │ │ │ └── valobj │ │ │ │ ├── TreeNodeLineVO.java │ │ │ │ ├── TreeNodeVO.java │ │ │ │ └── TreeRootVO.java │ │ │ ├── repository │ │ │ │ ├── IRuleRepository.java │ │ │ │ └── package-info.java │ │ │ └── service │ │ │ ├── engine │ │ │ │ ├── EngineBase.java │ │ │ │ ├── EngineConfig.java │ │ │ │ ├── EngineFilter.java │ │ │ │ └── impl │ │ │ │ └── RuleEngineHandle.java │ │ │ ├── logic │ │ │ │ ├── BaseLogic.java │ │ │ │ ├── LogicFilter.java │ │ │ │ └── impl │ │ │ │ ├── UserAgeFilter.java │ │ │ │ └── UserGenderFilter.java │ │ │ └── package-info.java │ │ └── user │ │ ├── model │ │ │ └── valobj │ │ │ └── UserVO.java │ │ ├── repository │ │ │ └── IUserRepository.java │ │ └── service │ │ ├── UserService.java │ │ └── impl │ │ └── UserServiceImpl.java │ └── xfg-frame-domain.iml ├── xfg-frame-infrastructure │ ├── pom.xml │ ├── src │ │ └── main │ │ └── java │ │ └── cn │ │ └── bugstack │ │ └── xfg │ │ └── frame │ │ └── infrastructure │ │ ├── dao │ │ │ ├── IUserDao.java │ │ │ ├── RuleTreeDao.java │ │ │ ├── RuleTreeNodeDao.java │ │ │ └── RuleTreeNodeLineDao.java │ │ ├── package-info.java │ │ ├── po │ │ │ ├── RuleTreeNodeLinePO.java │ │ │ ├── RuleTreeNodePO.java │ │ │ ├── RuleTreePO.java │ │ │ └── UserPO.java │ │ └── repository │ │ ├── RuleRepository.java │ │ └── UserRepository.java │ └── xfg-frame-infrastructure.iml ├── xfg-frame-trigger │ ├── pom.xml │ ├── src │ │ └── main │ │ └── java │ │ └── cn │ │ └── bugstack │ │ └── xfg │ │ └── frame │ │ └── trigger │ │ ├── http │ │ │ ├── Controller.java │ │ │ └── package-info.java │ │ ├── mq │ │ │ └── package-info.java │ │ ├── rpc │ │ │ ├── AccountService.java │ │ │ ├── RuleService.java │ │ │ └── package-info.java │ │ └── task │ │ └── package-info.java │ └── xfg-frame-trigger.iml └── xfg-frame-types ├── pom.xml ├── src │ └── main │ └── java │ └── cn │ └── bugstack │ └── xfg │ └── frame │ └── types │ ├── Constants.java │ ├── Response.java │ └── package-info.java └── xfg-frame-types.iml
领域
再回到前面让人疑惑的domain中
model对象的定义 valobj = VO,entity,Aggregate
repository 仓储的定义【含有PO】
service 服务实现
以上3个模块,一般也是大家在使用 DDD 时候最不容易理解的分层。比如 model 里还分为;valobj - 值对象、entity 实体对象、aggregates 聚合对象;
- 值对象:表示没有唯一标识的业务实体,例如商品的名称、描述、价格等。
- 实体对象:表示具有唯一标识的业务实体,例如订单、商品、用户等;
- 聚合对象:是一组相关的实体对象的根,用于保证实体对象之间的一致性和完整性;
再回到前面让人疑惑的domain中
model对象的定义 valobj = VO,entity,Aggregate
repository 仓储的定义【含有PO】
service 服务实现
以上3个模块,一般也是大家在使用 DDD 时候最不容易理解的分层。比如 model 里还分为;valobj - 值对象、entity 实体对象、aggregates 聚合对象;
- 值对象:表示没有唯一标识的业务实体,例如商品的名称、描述、价格等。
- 实体对象:表示具有唯一标识的业务实体,例如订单、商品、用户等;
- 聚合对象:是一组相关的实体对象的根,用于保证实体对象之间的一致性和完整性;
关于model中各个对象的拆分,尤其是聚合的定义,会牵引着整个模型的设计。当然你可以在初期使用 DDD 的时候不用过分在意领域模型的设计,可以把整个 domain 下的一个个包当做充血模型结构,这样编写出来的代码也是非常适合维护的。