33

DDD 领域驱动模型设计中的分层架构

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzA5OTI2MTE3NA%3D%3D&%3Bmid=2658338108&%3Bidx=2&%3Bsn=342ff23345eb65d47b9f0d54b04687b7
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在分解复杂的软件系统时,分层是我们最常用的手段之一。然而,在领域驱动设计中,层次和包的划分看起来与我们的结构又有一定区别,本文主要讨论DDD中的分层架构及每层的意义,以及与传统的三层架构的区别。

1. 为什么要分层

软件设计中分层的设计随处可见,但是分层能带来什么好处呢?或者说,我们为什么要考虑分层架构呢?

由于现实世界的复杂性,分层可以提供一个相对高层的视角来分解和简化我们的问题,此外分层也可带来可测试、可维护性、灵活性、可扩展性等方面的好处。

  • 简化复杂性,关注点分离,结构清晰;

  • 降低耦合度,隔离层次,降低依赖(上层无需关注下层具体实现),利于分工、测试和维护(可维护性);

  • 提高灵活性,可以灵活替换某层的实现;

  • 提高扩展性,方便实现分布式部署;

看起来十分简单,好像就是把系统划分为一定的层数,并把他们堆叠组织起来。但是,当落实到具体的实践时,如何划分、各层存在的意义、如何取舍以及相应的依赖关系却并没有想象中那么容易,边界的重合部分、不同场景下关注点、层次内部的具体分解以及层次的粒度等都是我们需要考虑的问题。

2. 什么是分层架构

2.1 分层的历史

最广为人知的应该就是经典的三层架构:展示层、业务逻辑层、数据访问层。

Martin Fowler在《企业应用架构模式》中也是类似的三层进行展开的:表现层,领域层,数据源层。

还有各种其他分层架构,这里就不一一描述了。

面对如此多的分层架构,我们不禁思考,他们分层的依据又是什么?能否抽象出一些相同点和不同点?又该在什么时候加入哪些合适的中间层?在实践中我们又该采取怎样的架构呢?

2.2 分层的本质

分层其实是把一系列相同或相似的对象进行分类放在同一层,然后根据他们之间的依赖关系再确定上下层次关系。可以看出,分层的核心在于分类和关联。

通常,我们可以将系统划分为变化较大的业务部分和相对稳定的技术部分;对于业务来说,又可划分为展示部分(前台)和内部处理逻辑(后台)两大部分;展示又可分为数据/页面部分和接口部分。如此不断的进行细分和抽象,我们可以迭代出更细粒度的分类/层次,如下所示:

业务:需要重点关注,我们的目的也是分离出具体的业务领域逻辑:

  • 外部展示(表现层/接口层):数据、页面(web)、远程接口(interface/api)

  • 内部逻辑处理:应用逻辑(应用层/服务层)、具体业务逻辑(领域层)

技术:相对稳定,具体业务无关(基础设施层)

  • 数据访问(数据访问层)

  • 日志、安全、异常、缓存等

当然,分类并不唯一,基于不同的视角我们可能会有不同的分类标准。比如数据访问层也可以归类到业务相关/内部逻辑处理的部分,因为可能涉及到一些对具体业务表的操作。

此外,根据问题领域和解决方案的复杂程度,我们可以有不同的层次。比如在业务不太复杂时,我们可以把应用层和领域层合并为一层。

3. DDD经典分层架构

上面我们在分析分层的本质时也提到了一些基本的层次和分类标准,但那只是一个非常粗粒度的划分。

在实际决策时,我们需要知道各层的职责、意义以及相应的场景;而落实到代码层面时,我们还需要知道各层所包含的具体内容、各层的一些常见的具体策略/模式、层次之间的交互/依赖关系。

首先我们来看一下Evans在《领域驱动设计》中提到的分层架构。

jiANren.jpg!web

image

问:为什么要分成这样的四层?

分层主要目的是为了简化复杂性,系统中最复杂的部分应该就是我们的业务逻辑。当系统交互或者工作流比较复杂时,我们会考虑从业务逻辑中抽出这部分作为应用层。而各个领域内的代码则化为领域层,这样层级结构更加清晰。

3.1 用户界面层/表示层

用户界面层负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。

该层包含与其他应用系统(如web服务、RMI接口或web应用程序以及批处理前端)交互的接口与通信设施。

它负责输入参数的解释、验证以及转换。另外,它也负责输出参数的序列化,如通过HTTP协议向web浏览器或web服务客户端传输HTML或XML,或远程Java客户端的DTO类和远程外观接口的序列化。

可以看出,该层的主要职责是与外部用户(包括web服务、其他系统)交互,如接受用户的反馈,展示必要的数据信息。主要包含web部分和远程服务部分等。

web部分一般包含常见的Servlet,Controller等组件,而远程接口部分主要由Facade、DTO和Assembler等构成。

  • Facade:远程外观,一个粗粒度的外观,不含任何领域逻辑

  • DTO:数据传输对象

  • Assembler:对象组装器,负责数据传输对象与领域对象相互转换,不对外暴露

问:参数的校验为什么在用户界面层?领域层的校验和用户界面层的校验有什么不同?

校验应该取决于校验的内容,一般推荐尽早校验,不过这里主要是进行一些简单的、不涉及业务规则的校验。具体的业务规则的校验放在领域层。

问:为什么需要DTO?DTO和VO是同一个东西吗?

领域对象关系比较复杂,很难序列化,而且用户很多时候并不需要整个模型,大部分时候需要的只是其中的一部分内容,DTO可以有效减少网络调用的开销。此外,领域模型内部的逻辑也无需暴露给外部。

DTO一般用于远程服务,如果是内部使用的话,一般可以直接使用领域对象。

VO中有前端状态信息,比如成功失败等。

问:为什么需要Assembler?

主要目的是解耦,负责数据传输对象和领域对象之间的相互转换。BeanUtils也可以做到相应的功能(dozer相对好一些),不过Assembler更为清晰,安全与可控,缺点在于手工代码量稍多。

3.2 应用层

应用层定义了软件要完成的任务,并且指挥表达领域概念的对象来解决问题。该层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要通道。

应用层要尽量简单。它不包含任务业务规则或知识,只是为了下一层的领域对象协助任务、委托工作。它没有反映业务情况的状态,但它可以具有反映用户或程序的某个任务的进展状态。

应用层主要负责组织整个应用的流程,是面向用例设计的。该层非常适合处理事务,日志和安全等。相对于领域层,应用层应该是很薄的一层。它只是协调领域层对象执行实际的工作。

综上所述,应用层是表达user case和user story的主要手段,主要用于协调领域模型与其他应用组件的工作(并不处理业务逻辑)。

应用层中主要组件是Service,因为主要职责是协调各组件工作,所以通常会与多个组件交互,如其他Service,领域对象,Repostitory等。

一种比较常见的做法是:应用层通常接受来自用户界面层的参数,再通过Repostitory获取到聚合示例,然后执行相应的命令操作(很薄的一层)。

问:为什么要有应用层?

业务比较复杂时,我们会从业务逻辑中拆分出应用层和领域层。

如果在领域对象中事先针对具体应用的逻辑,会降低应用之间的可重用性。

此外,如果将来需要加工作流之类的工具来实现应用逻辑,如果之前是混杂在一起的话则不好拆分。

3.3 领域层/模型层

领域层主要负责表达业务概念,业务状态信息和业务规则。

Domain层是整个系统的核心层,几乎全部的业务逻辑会在该层实现。

领域模型层主要包含以下的内容:

  • 实体(Entities):具有唯一标识的对象

  • 值对象(Value Objects): 无需唯一标识

  • 领域服务(Domain Services): 一些行为无法归类到实体对象或值对象上,本质是一些操作,而非事物

  • 聚合/聚合根(Aggregates & Aggregate Roots): 聚合是指一组具有内聚关系的相关对象的集合,每个聚合都有一个 rootboundary

  • 工厂(Factories): 创建复杂对象,隐藏创建细节

  • 仓储(Repository): 提供查找和持久化对象的方法

关于各个元素的具体含义、职责以及相关误区,可参考领域建模核心概念解析.

对于这些具体的对象,可定义一些标准领的 Annotation 来规范。

3.4 基础设施层

基础设施层为上面各层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件。

基础设施层以不同的方式支持所有三个层,促进层之间的通信。

基础设施包括独立于我们的应用程序存在的一切:外部库,数据库引擎,应用程序服务器,消息后端等。

作为基础设施层, InfrastructureInterfacesApplicationDomain 三层提供支撑。所有与具体平台、框架相关的实现会在 Infrastructure 中提供,避免三层特别是 Domain 层掺杂进这些实现,从而“污染”领域模型。 Infrastructure 中最常见的一类设施是对象持久化的具体实现。

问: Repository 作用是什么?和 DAO 的关系

之前对 Repository 也曾有过误解(在我们的系统中有一个 repository 层位于 daoservice 之间)。

DAO 主要是从数据库表的角度来看待问题的,并且提供 CRUD 操作(只是对数据库表的一个封装),是一种面向数据处理的风格(事务脚本);

Repository (资源库)和 Data Mapper (数据映射器)更加面向对象,通常用于领域模型中。

因为数据访问层的暴露可能会破坏对象的封装性,对象的关系和数据一致性也难以维护,所以 

应该尽量避免在领域模型中使用 DAO 模式,推荐使用聚合本身来管理业务逻辑。

4. 模型的形态

不同的架构、不同的层、不同的应用场景中有着不一样的建模需求,因此表达相同概念的模型可能会有不同的形态,例如:

  • 充血模型:领域模型架构中包含了领域逻辑和领域属性的领域模型。

  • 失血模型:传统三层架构中只有get/set方法,没有业务逻辑的POJO对象。

  • 贫血模型:类似充血模型,但是不包括持久化相关逻辑。

  • PO(Persistant Object):持久化对象,即DAO从JDBC取出来的对象。传统三层架构中,PO即POJO组件中的对象,存在于DAO和Service之间。

  • DO(Domain Object):领域对象,领域模型架构中,PO从数据库取出来后,有一个“重建”的概念,即根据数据还原实体,这个被还原的实体就是DO,存在于DAO和Service之间。

  • DTO(Data Transfer Object):数据传输对象。对传统三层架构来说,该对象存在于Service和Controller之间。PO到DTO的转换可以在Service或Controller中实现。

  • VO(View Object):视图对象。Controller在返回DTO给视图时,可能还需要包括状态信息例如操作成功/失败的状态码、提示文本等。这时就需要在DTO外面再包一层,即View Object。该对象存在于Controller和Web之间,由Controller进行装配

参考文档:

https://my.oschina.net/hosee/blog/919426

Kotlin 开发者社区

国内第一Kotlin 开发者社区公众号,主要分享、交流 Kotlin 编程语言、Spring Boot、Android、React.js/Node.js、函数式编程、编程思想等相关主题。

IF3MZfm.jpg!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK