谈谈领域驱动设计对三层架构的优化
谈谈领域驱动设计对三层架构的优化
作者:邵涯语
日期:2023年3月13日
1、领域驱动设计
1.1、什么是领域驱动设计
领域驱动设计(Domain Driven Design,简称DDD)是一种软件开发方法论,它强调将软件开发的重心放在对业务领域的理解和建模上,通过与领域专家紧密合作,将领域知识转化为高质量的软件模型。领域驱动设计的目标是实现高内聚、低耦合、易于维护、可扩展的软件系统,同时满足业务需求,帮助开发人员更好地理解和实现业务功能。
在领域驱动设计中,核心思想是将业务领域作为软件开发的中心,将领域专家和开发人员的知识进行整合和协作,通过深入理解业务领域的语言和概念,构建一个清晰、准确的领域模型,并将领域模型贯穿于整个软件系统的设计和实现过程中。
领域驱动设计还强调使用通用语言(Ubiquitous Language)来描述业务领域,通用语言是指开发团队和领域专家共同使用的语言,它能够确保领域模型的一致性和准确性,同时也可以避免开发人员在不同的环节中使用不同的术语,导致理解不一致和沟通不畅的情况。
综上所述,领域驱动设计是一种将业务领域和软件开发进行紧密整合的方法,通过深入理解和建模业务领域,构建清晰、高质量的领域模型,从而实现软件系统的高内聚、低耦合、易于维护和可扩展。
1.2、我为什么需要领域驱动设计
在我经常写的系统中,用Spring Boot开发后端系统,通常简历的包目录如下
--controller #主要写前后端交互的接口
--service # service,主要写逻辑交互
----impl # service 的接口实现类
--mapper # mapper 主要和数据库进行交互
--po # 主要存储和数据库交互的实体
--vo # 主要存储和
--config # 主要写项目中一些配置
--util # 写项目中的工具类
这种包结构在系统发展过程中,由于业务逻辑的增加,导致service层变得臃肿,一个service的方法可能达到上百行代码,难以维护。同时,由于service和controller难以分开,一些本应在controller中完成的校验却被放在了service中,导致两者的边界不清晰。同样地,在多人合作开发时,有些开发者习惯在service层中进行SQL拼接操作,这也导致了service层与mapper层的边界不清。
在项目初期就应该构建良好的包结构,根据领域模型将业务逻辑划分到不同的层次中。可以采用分层架构或者领域驱动设计中的四层架构,将业务逻辑分离到不同的层次,使得各层职责明确,代码量合理。
1.3、领域驱动设计的优点和缺点
领域驱动设计 (DDD) 是一种软件开发方法论,它强调将业务领域和软件开发进行紧密整合,通过深入理解和建模业务领域,构建清晰、高质量的领域模型,从而实现软件系统的高内聚、低耦合、易于维护和可扩展。其优点和缺点如下:
优点:
- 提高开发效率:领域驱动设计将领域知识与软件设计过程紧密结合,有效提高了开发效率。
- 易于理解和维护:领域驱动设计强调使用通用语言来描述业务领域,使得业务领域模型易于理解和维护。
- 适应业务变化:领域驱动设计强调迭代开发和持续改进,使得软件系统能够更好地适应业务变化。
- 高质量的领域模型:领域驱动设计通过与业务专家紧密合作,能够构建出高质量的领域模型,从而保证软件系统的正确性和可靠性。
缺点:
- 学习成本高:领域驱动设计对开发人员的要求较高,需要掌握大量的领域知识和设计技巧,因此学习成本较高。
- 需要与业务专家紧密合作:领域驱动设计要求开发人员与业务专家紧密合作,这需要开发团队和业务专家之间具有良好的沟通和协作能力。
- 设计复杂性:领域驱动设计强调构建复杂的领域模型,这可能会增加系统的设计复杂性。
- 不适合所有项目:领域驱动设计适合大型、复杂的软件系统,对于小型、简单的项目来说可能过于繁琐。
1.4、总结
综上的方案,我选择开始拥抱DDD领域驱动设计,总结一个过度从传统的四层架构开发到DDD领域驱动的四层架构。去掉DDD领域驱动中的一些复杂的设计方案,能提升项目的开发效率和维护同时也需要降低学习成本和系统的设计复杂程度。
2、DDD领域驱动中关键名称
2.1、领域驱动设计中的五个“域”
在领域驱动设计中,一个系统的业务可以被划分为多个领域,而每个领域又可以被划分为多个子域,这些领域和子域的划分是基于业务实体和业务逻辑的划分。
- 领域(Domain):是指一组相关联的业务实体和业务逻辑的集合,是系统中的核心部分,体现了业务价值的本质。一个领域通常包括多个子域。
- 子域(Subdomain):是指领域中的一个具体的业务子集,是领域的一个组成部分。每个子域具有独立的业务模型、语言和约束,可以由一个团队独立负责。
- 核心域(Core Domain):是指系统中最为重要、最具价值的领域,是公司的核心竞争力所在。通常需要投入大量的人力和物力进行开发和维护,是系统中最为复杂和困难的部分。
- 通用域(Generic Subdomain):是指领域中与业务价值不直接相关,但是多个领域都需要用到的通用功能。通常不是核心领域,但是也需要进行开发和维护。
- 支撑域(Supporting Subdomain):是指领域中的一些辅助功能,如管理、运营、监控等,对业务价值的直接贡献较小,但是对系统的可靠性和稳定性有很大的影响。
2.2、限界上下文
在领域驱动设计中,限界上下文(Bounded Context)是指一个特定的领域内部的业务边界,限制了业务逻辑的范围和意义。一个限界上下文包含了该领域内部的业务模型、语言、规则和约束等,同时也提供了与其他限界上下文之间交互的接口和协议。
限界上下文的作用是将复杂的业务系统划分成小的、可管理的部分,每个限界上下文都有自己的职责和边界,可以由不同的团队负责开发和维护。限界上下文之间的交互通过明确的接口和协议进行,可以避免系统中的混乱和冲突。
限界上下文的设计需要根据实际业务情况进行,通常需要考虑以下几个方面:
- 领域边界:确定限界上下文的边界,即该限界上下文所包含的业务范围。
- 业务模型:定义限界上下文内部的业务模型,包括实体、值对象、聚合根等。
- 语言和规则:限界上下文内部的业务语言和规则需要与外部保持一致,同时也需要符合该领域的特定需求。
- 接口和协议:限界上下文之间的交互需要定义明确的接口和协议,确保交互的正确性和可靠性。
限界上下文的设计需要与其他领域模型紧密协作,确保整个系统的一致性和可扩展性。
3.3、实体和值对象
在领域驱动设计中,实体(Entity)和值对象(Value Object)是常用的两种建模概念,用于描述业务领域中的不同类型的对象。
实体通常是具有唯一标识符和生命周期的对象,它们可以有自己的属性和方法,同时也可以包含其他实体或值对象。实体通常表示业务中重要的核心概念,例如订单、客户、产品等。在领域驱动设计中,实体的设计需要考虑到实体的生命周期、标识符的生成和管理、实体之间的关系等。
值对象是没有标识符的对象,它们通常用于描述实体的属性或特征。与实体不同,值对象的相等性是基于它们的属性值而不是标识符来定义的。例如,一个包含姓名、地址和电话号码的联系人对象可以被建模为值对象,因为它没有独立的生命周期和标识符。在领域驱动设计中,值对象通常被设计为不可变对象,因为它们的相等性是基于属性值而不是对象状态的。
在实践中,实体和值对象的设计需要根据具体的业务需求进行,通常需要考虑以下几个方面:
- 标识符和生命周期:实体通常具有独立的标识符和生命周期,而值对象则不需要。
- 可变性:实体通常是可变的,可以通过修改属性或执行业务操作来改变它们的状态,而值对象通常是不可变的。
- 相等性:实体的相等性通常基于标识符和生命周期来定义,而值对象的相等性通常基于属性值来定义。
- 聚合:实体和值对象通常都是聚合的一部分,聚合是一种将相关对象组合在一起的方式,用于实现业务需求。
总之,实体和值对象是领域驱动设计中常用的建模概念,用于描述业务领域中不同类型的对象。它们在设计中具有不同的特点和应用场景,需要根据具体的业务需求进行选择和设计。
3.4、聚合和聚合根
在领域驱动设计中,聚合是一组相关对象的集合,这些对象之间具有内聚性和一致性,并且由一个聚合根对象进行管理和控制。聚合根是聚合中最重要的对象,负责保证聚合中对象的一致性和完整性。
聚合的目的是将一组相关对象视为一个整体,使其更易于管理和维护。聚合内的对象可以相互引用,但聚合外的对象不能直接引用聚合内的对象,必须通过聚合根进行访问。
聚合的设计原则包括:
- 保持聚合的内聚性和一致性;
- 通过聚合根实现聚合的管理和控制;
- 避免跨聚合的引用;
- 限制聚合的大小,尽量保持小而简单。
在实现聚合时,可以使用各种技术和模式,例如对象组合、注入依赖关系、工厂模式等。同时,聚合的设计需要结合具体的业务需求和架构设计进行考虑。
3、领域设计四层架构
当谈论领域驱动设计时,通常会提到四层架构,也称为领域驱动设计架构,该架构包括表示层(Presentation Layer)、应用层(Application Layer)、领域层(Domain Layer)和基础设施层(Infrastructure Layer)。下面分别介绍每层的功能职责、设计原则和实现方法:
3.1、表示层
- 功能职责:表示层是用户和系统之间的接口,主要负责展示数据和接受用户输入。其职责包括处理用户请求、验证用户输入、向用户展示信息等。
- 设计原则:表示层应该与应用层、领域层和基础设施层相互独立。其主要职责是将用户输入转换为应用层可以理解的格式,并将应用层的输出转换为用户可以理解的格式。表示层应该尽可能简单,避免包含业务逻辑。
- 实现方法:常见的实现方法包括 MVC、MVVM、MVP 等模式。使用模板引擎和前端框架可以简化表示层的开发过程。表示层通常是使用 Web 技术实现的,如 HTML、CSS、JavaScript、Spring MVC 等。
3.2、应用层
- 功能职责:应用层是业务逻辑的实现层,主要负责协调领域层和基础设施层。其职责包括接受用户请求、处理业务逻辑、调用领域层的服务、管理事务等。
- 设计原则:应用层应该对领域层进行封装,使得领域层更加专注于业务领域。应用层的设计应该符合单一职责原则,每个应用层服务应该只负责一个业务逻辑。应用层不应该包含持久化相关的逻辑。
- 实现方法:通常使用面向服务的架构(SOA)或面向对象的架构(OOA)实现应用层。使用依赖注入和领域事件等技术可以简化应用层的开发过程。应用层通常是使用 Java、C# 等语言实现的。
3.3、领域层
功能职责
领域层是领域设计四层架构的核心,负责实现业务逻辑,定义业务实体、值对象、聚合等领域模型,以及业务流程的定义和实现。领域层主要包括以下职责:
- 实现领域模型和业务流程,确保业务逻辑正确性和可维护性。
- 提供领域服务,包括查询和操作等,供应用层调用。
- 管理领域对象的状态和生命周期,保证数据一致性和业务正确性。
- 集成应用层和基础设施层,是整个系统的核心。
设计原则
- 高内聚、低耦合:领域层内部各个模块之间高度内聚,但是与应用层和基础设施层的耦合尽量减少。
- 领域驱动设计(DDD):基于业务场景的需求,通过领域模型驱动设计,确保系统的业务逻辑正确性和可维护性。
- 单一职责原则(SRP):一个领域模型只关注一个领域概念或业务流程,确保模型的清晰和简单。
实现方法
- 定义业务实体和值对象:通过领域模型对业务进行建模,将业务概念映射成领域实体和值对象,并定义它们的属性和行为。
- 定义聚合:将业务实体和值对象进行聚合,形成聚合根和聚合内的实体和值对象,确保数据一致性和业务完整性。
- 定义领域服务:根据业务流程,定义领域服务的接口和实现,提供业务查询和操作等服务。
- 定义领域事件:在领域层中定义领域事件,通过发布-订阅模式,将领域事件与外部系统进行集成。
3.3、基础设施层
- 功能职责:提供技术支持,将领域层解耦合,使得领域层更加专注于业务逻辑的处理,提高代码的可维护性和可测试性。
- 设计原则:遵循面向接口编程的原则,将领域层和基础设施层进行解耦,使得领域层不依赖于具体的技术实现。
- 实现方法:通过DAO、DTO、框架、组件等方式实现基础设施的功能,如数据持久化、消息队列、缓存等。同时,还需要提供一些通用的功能,如日志、异常处理、安全认证等。
4、项目实战规范
落地实现目标:在原本理解的三层架构基础上,结合领域驱动设计实现下面目标
1、降低学习成本,提升代码规范
2、向着领域驱动设计迈步。(如果你想完整实现DDD领域驱动设计,可以跳过)
4.1、三层架构和DDD的四层架构
传统的三层架构和领域驱动设计的四层架构是两种不同的架构设计模式,主要的区别在于它们的关注点不同。
传统的三层架构包括表示层、业务逻辑层和数据访问层,它的设计目标是将系统划分为独立的、高内聚、低耦合的模块,使得每个模块的职责清晰、易于维护和升级。在这种架构中,业务逻辑层是系统的核心,它处理所有的业务逻辑,而表示层和数据访问层则分别负责与用户交互和访问数据。
领域驱动设计的四层架构则更加强调领域模型的设计和实现,它包括表示层、应用层、领域层和基础设施层。在这种架构中,领域层是系统的核心,它包含了系统的领域模型和业务逻辑,而应用层负责协调各个领域层的操作,提供应用程序的服务接口,表示层负责与用户交互,基础设施层负责提供与外部系统的交互和数据存储等服务。
传统的三层架构注重的是模块的职责划分和数据的流动,而领域驱动设计则更加注重领域模型的设计和实现。领域驱动设计中的领域层可以看作是传统的三层架构中的业务逻辑层,但是它的职责更加明确和专注,不同的领域之间也可以相对独立地进行设计和实现,而不会像传统的三层架构那样存在交叉耦合的问题。此外,领域驱动设计中还引入了一些新的概念,如聚合、限界上下文等,以更好地实现领域模型的设计和实现。
4.2、基于DDD指导的系统代码模型
4.2.1、项目代码的一级目录
项目的一级目录主要包含下面四个目录:
1、interfaces (用户接口层)
它主要存放用户接口层与前端交互、展现数据相关的代码。
2、application (应用层)
它主要存放应用层服务组合和编排相关的代码。
3、domain (领域层)
它主要存放领域层核心业务逻辑相关的代码。
4、infrastructure (基础设施层)
它主要存放基础资源服务相关的代码
4.2.2、各目录结构
(1)、interfaces
接口层主要包含两个主要的包,
1、controller api接口,这个是由我们技术所决定的,使用Spring MVC。这个包主要管理对外提供的接口服务
2、vo/dto 主要存储和前端交互的值对象,
(2)、application
1、event 事件管理,主要做事件的发送和接收
2、service 应用层的服务,主要是对跨领域的事务和复杂的业务进行拆解成不同的domain服务
后缀名用Asr
服务实现后缀名使用AsrImpl
(3)、Domain
领域层下面的目录是具体的领域的实现,领域的目录结构如下
1、service
service中主要包含当前领域的服务,后缀名用Service,实现的后缀名用ServiceImpl
service 主要是和数据库mapper层做交互的,当然也包括构建数据库增删改查条件
2、mapper
主要负责和数据库交互
3、po
和数据库交互的值对象
4、entity
entity 主要做实体存在,不参与数据库交互,主要提供方法。
如:UserEntity,校验数据库获取到的数据是否满足规则,校验前端传入的Vo/Dto是否满足输入规范。
除了校验,还包括根据Po对象,创建不同领域需要的值对象。
总之,entity对象,没用属性,但是有存在不同层调用Vo/Dto/Bo/Po值对象的方法。
(4)、基础设施层
基础设施主要提供项目的配置和项目的工具
1、config 主要写项目中的配置,如Api文档配置、安全规则、全局异常处理、日志配置等等
2、util 主要提供项目中必要且不是领域层的实体的方法工具,如果用户校验,这个应该放在领域的实体层中写,不要写在工具中。
比如常见的工具。数据库分页工具、Redis工具、Spring Boot工具等等。
4.2.3、项目结构扩展
上面的介绍只是项目包的骨架,像/interfaces/controller、/interfaces/vo、/infrastructure/config和/infrastructure/util目录下面,根据不同的类型或则领域进行再次分包,也是可行的。
项目中缺少一个工具就是Vo/Dto/Bo/Po转换工具,我们大部分都是使用DozerMapper这个工具对对象进行拷贝,如果没用用工具拷贝,开发人员可以使用实体进行转换。
4.3、总结
这项的项目构建,离DDD领域驱动设计还是有一段距离,但是这样的项目架构方案,能很好的解决传统的三层架构中代码不规范清晰的问题,提升了项目的代码管理和开发效率,同样适合低成本和小项目。
实现这种架构方案,最重要还是看项目的业务掌握程度,能把业务拆解成不同的领域。这是整个项目的重点。
道阻且长,在实践中不断推进项目的合理架构,才是正确的目标。一万个读者有一万个哈姆雷特,不同的人,不同的项目,对DDD领域驱动设计的落地都有不同的架构方案,这里只是提出一种适合过渡,且方便管理代码的方案。
5、参考
[DDD 微服务落地实战](http://learn.lianglianglee.com/专栏/DDD 微服务落地实战)
[领域驱动设计在互联网业务开发中的实践](https://tech.meituan.com/2017/12/22/ddd-in-practice.html?utm_source=wechat_session&utm_medium=social&utm_oi=698166473230680064)
欢迎留言探讨