项目终于用上了 DDD 领域驱动,太强了

news2024/12/21 18:36:43

在公司对支付业务、结算业务、资金业务使用DDD进行领域建模的两年,得到了许多好评,也面对过不少质疑,总体来说还是能收获不少,这对团队成员理解业务起着很大作用。近半年一直在研究DDD的落地实战,如今已修得阶段性成果,迫不及待与大家分享我的落地经验。

DDD分为战略设计战术设计。一般来说,领域建模是属于战略层的,而DDD工程落地是属于战术层的,两者是否结合使用,视实际情况而定,比如传统的MVC架构也能使用DDD进行领域建模,DDD架构最好是先做DDD领域建模。

最新上线的一个微服务——内部交易中心,我们使用了DDD架构来落地,希望看完对大家有启发。

工程架构分层理论

在工程落地之前,我们有必要先了解下主流的工程架构或架构思想都有哪些,对这些理论有所了解的,也可以直接跳过看下一个部分。

1、经典DDD四层架构

在该架构中,上层模块可以调用下层模块,反之不行。即:

  • Interface ——> application | domain | infrastructure
  • application ——> domain | infrastructure
  • domain ——> infrastructure

分层作用:

  • 用户界面层/表现层:负责向用户显示解释用户命令
  • 应用层:定义软件要完成的任务,并且指挥协调领域对象进行不同的操作。该层不包含业务领域知识
  • 领域层/模型层:系统的核心,负责表达业务概念,业务状态信息以及业务规则。即包含了该领域(问题域)所有复杂的业务知识抽象和规则定义。该层主要精力要放在领域对象分析上,可以从实体,值对象,聚合(聚合根),领域服务,领域事件,仓储,工厂等方面入手
  • 基础设施层:一是为领域模型提供持久化机制,当软件需要持久化能力时候才需要进行规划;二是对其他层提供通用的技术支持能力,如消息通信,通用工具,配置等的实现;

2、整洁架构思想

整洁架构(Clean Architecture)是由Bob大叔在2012年提出的一个架构模型,顾名思义,是为了使架构更简洁。

依赖规则:用一组同心圆来表示软件的不同领域。一般来说,越深入代表你的软件层次越高。外圆是战术是实现机制,内圆的是核心原则。

这条规则规定软件模块只能向内依赖,而里面的部分对外面的模块一无所知,也就是内部不依赖外部,而外部依赖内部。同样,在外面圈中使用的数据格式不应被内圈中使用,特别是如果这些数据格式是由外面一圈的框架生成的。

这样做的最大好处是当系统的外部模块不得不改变时(比如,替换已有的过时的数据库系统),系统的内层模块不需要做任何改变。

3、六边形架构

六边形架构(Hexagonal Architecture),又叫做端口适配器模式,是由Alistair Cockburn在2005年提出的。

六边形架构将系统分为内部(内部六边形)和外部,内部代表了应用的业务逻辑,外部代表应用的驱动逻辑、基础设施或其他应用。内部通过端口和外部系统通信,端口代表了一定协议,以API呈现。

一个端口可能对应多个外部系统,不同的外部系统需要使用不同的适配器,适配器负责对协议进行转换。这样就使得应用程序能够以一致的方式被用户、程序、自动化测试、批处理脚本所驱动,并且,可以在与实际运行的设备和数据库相隔离的情况下开发和测试。

4、菱形架构

作用于限界上下文的菱形对称架构从领域驱动设计分层架构与六边形架构中汲取了营养,通过对它们的融合形成了以领域为轴心的内外分层对称结构。

内部以领域层的领域模型为主,外部的网关层则根据方向划分为北向网关南向网关。通过该架构,可清晰说明整个限界上下文的组成:

  • 北向网关的远程网关
  • 北向网关的本地网关
  • 领域层的领域模型
  • 南向网关的端口抽象
  • 南向网关的适配器实现

限界上下文以领域模型为核心向南北方向对称发散,从而在边界内形成清晰的逻辑层次,前端UI并未包含在限界上下文的边界之内。每个组成元素之间的协作关系表现了清晰直观的自北向南的调用关系。

5、CQRS

CQRS(Command Query Responsibility Segregation)意为命令查询职责分离,它是一种与领域驱动设计 (DDD) 和事件溯源相关的架构模式。Greg Young在2010年创造了这个术语,CQRS的内容基于Bertrand Meyer的CQS设计模式。

CQRS架构将写入和读取分开,它提出了单独的 API,一个专用于更改应用程序状态的命令路由,另一个专用于返回有关应用程序状态信息的查询路由。

工程架构分层设计

基于各个架构有其自己的优缺点,我们结合公司的现状,取其长避其短,融合一套适合自己的架构。

  • 以经典DDD四层架构为骨架,其他优秀架构思想作指导
  • CQRS命令/查询职责分离,应用到DDD应用层,处理复杂操作/复杂查询
  • 整洁架构应用到DDD领域层与基础设施层,接口与实现拆到不同层,把技术代码与业务代码分离
  • 菱形架构指导我们,内部以领域层的领域模型为主,向南北两个方法发散——北向网关(领域层以上)提供本地网关(如Controller、MQListener)与远程网关(如API包);南向网关(领域层以下)负责端口抽象(如仓库接口)与适配器实现(如外部API封装实现)
  • 公司的Base框架在dal包封装了基础CRUD接口,应用到数据访问层内,作为领域层与基础设施层的粘合剂,简化链接

当然,任何事物有其两面性,融合各个框架后,也有其优缺点——

优点:通过分离业务与技术代码,有利于业务迭代升级维护;业务驱动而非技术/数据驱动,通过写代码就能积累一定的业务知识;将领域知识和技术知识分类,从而提高代码的可重用性。

缺点:对从业人员业务分析能力较高,难以从经典MVC架构转变过来;层级较多,写代码前需考虑清楚逻辑应该写在哪一层;规则较多,没有MVC架构灵活,不适用于简单业务系统;学习成本与转移成本比较高,需要对DDD有更好的理解和更长的设计时间(资金组践行DDD领域建模2年)。

工程代码构建案例

看代码之前我们先看下领域建模:

通过领域模型分析,内部交易中心分为内部调货、规则中心、内部出入库、内部销售、内部采购这五大模块,每一个模块对应DDD就是一个聚合,所有聚合形成一个DDD的限界上下文(内部交易上下文),之前的文章提到,限界上下文就是我们划分微服务的一个重要依据。

接下来,我们结合DDD架构图与领域建模,看看工程代码应该怎么放。

基于Maven的DDD工程,顶层结构我们按api、service划分为两个module。

api包的作用:

  • api包的定位是跨服务的顶层契约,service包所有层都可以依赖api包
  • api包定义了对外透出的枚举/常量、入参、出参、API接口等,为了方便使用api类,feign层不作业务划分
  • api包只定义契约不写业务逻辑,避免因业务逻辑变更引发的api包升级

service包的作用:

  • service包是工程的顶层实现,DDD四层架构在service包体现
  • Application程序入口与DDD的四层处于同一目录

此外,针对service包还有另一种主流的module划分方式——直接把service包的api、application、domain、infrastructure作为四个独立的module,优点是能通过pom依赖的方式来限制层与层之间的依赖,开发人员能在编码阶段发现依赖问题及时修正,但缺点也明显——不够灵活,工程也会变得较重。

1、接入层(api)

接入层又叫用户接入层,主流用interface或api命名,基于包默认按字母排序的原因,我建议使用“api”来命名接入层,但要注意,service包的api层与api包是不同的作用。

  • 接入层是很薄的一层,负责直接对接前端请求或feign实现(facade里的Controller)、数据转换(assembler),入参/出参等契约类(request/response)统一定义在顶层的api包
  • Controller负责对数据做前置校验,具体业务逻辑则交给应用服务或领域服务实现,可直接调用应用服务方法或领域方法
  • 业务划分在接入层不明显,更多是基于前端模块进行划分Controller,且业务复杂时必然存在领域交叉,故facade下没有再细分业务包
  • assembler数据转换负责处理复杂的数据转换,简单的数据转换可显式调用工具类的转换方法

2、应用层(application)

应用层主要作用是业务编排、转发、校验等,处理跨聚合、领域事件逻辑,复杂操作/复杂查询也在此层体现(CQRS)。

  • 应用服务AppService是一种简单逻辑封装,接入层无法直接调用领域层拿到结果的,可在此层编排封装聚合方法
  • 应用层可依赖领域层,但不可依赖接入层,所以传参进应用层要么是基础类型,要么在接入层assembler做一层转换,要么入参出参定义在api包
  • 事件一般情况是跨聚合或跨服务的,所以事件定义在应用层,在应用层处理事件的发布/订阅
  • 接入层可直接调用领域层,不经过应用层

3、领域层(domain)

领域层或称为模型层,系统的核心,负责表达业务概念、业务状态信息以及业务规则,包含了该领域所有复杂的业务知识抽象和规则定义。

  • 领域层只表达业务,不写技术代码,在业务上不依赖其他层
  • 领域聚合以业务来命名包,聚合内包含该聚合下所有模型(DO对象)、仓库接口、领域服务
  • 领域模型model是领域聚合下的业务核心模型,以XxxDO命名,依旧采用贫血模型,只包含少量原子性操作,不包含跨模型数据处理、持久化操作等
  • 仓库使用repository命名,领域层只定义仓库接口,不写仓库实现
  • 领域工厂factory与设计模式里的工厂模式不同,领域工厂主要负责领域对象的复杂构建,如领域对象生成、属性填充等,由于存在跨聚合的情况,所以factory包并不在聚合内,与领域聚合同层级
  • 外部API接口、外部框架代码做一层浅封装,放在external聚合包下,以ExXxxService命名接口,实现类还是在基础设施层,起接口防腐作用

因为领域建模最终体现在领域层内,在我们建模时就要考虑领域层的代码如何写。

  • 领域建模时只表达核心属性与核心行为
  • 聚合内跨多个模型的复杂业务逻辑,写在领域服务内
  • 领域模型的方法只写原子性的操作,但不包括CRUD持久化操作

一些难点:

  • 无法实现模型的“所建即所得”,复杂代码无法通过领域模型的简单几个方法表达完整
  • 模型只能表达核心的业务行为,所谓的充血模型在落地时可能更多地拆分到领域工厂、领域服务、应用服务中实现

4、基础设施层(infrastructure)

基础设施层作为工程的基础设施使用,编写与业务无关的代码,如技术框架、工具类,此外还有一个重要的功能,要写仓库的实现类、外部服务的实现类。

  • 基础设施层的仓库(repository)实现了领域层定义的仓库接口,数据访问层(dao)也定义在仓库下,数据库实体(PO对象)定义在entity,以XxxPO或XxxEntity命名,这里遵循了公司框架的命名方式,使用了XxxEntity
  • param是比较特殊的一层,该类一般定义查询数据库的参数。基于公司的Base框架,repository定义接口时依赖了param对象,按道理领域层不应依赖基础设施层(DIP原则),但Param又跟PO对象息息相关,所以把param对象放在了基础设施层
  • 基于Clean架构原则,其他框架性代码、工具类、配置类都放在基础设施层,业务代码与技术代码分离后,万一升级技术代码,对业务代码做最少改动

我们再来看一下全貌:

通过实际案例,总结以下重要几点:

  • 领域层是业务最核心的一层,聚合之间的边界需要划分清晰,而接入层、应用层涉及跨聚合,基础设施层关注仓储实现与技术框架,所以我们只在领域层划分业务包,对应领域建模按聚合划分边界,并定义领域模型的仓储接口
  • 充血模型建模,贫血模型落地,把核心业务行为按需划分到领域模型(原子性、非持久化)、领域工厂(构建模型)、领域服务(跨模型)或应用服务(跨聚合、事件)中
  • 使用接口分离业务代码与技术性代码,当业务迭代时,修改领域层和基础设施层的仓库实现即可;当技术框架升级时,修改基础设施层即可,不至于把业务代码也修改一遍,减少出错成本

DDD工程落地考虑的是代码的归类划分问题,重点在于业务边界的识别、业务和技术代码的解耦。写代码前需要考虑清楚不同的代码应该写到哪里,结合前人优秀的工程架构思路与公司当前的技术架构,整合一套灵活的、适合我们自己的DDD,不能照搬,更不能为了DDD而DDD。

疑难分析

1、用充血模型还是贫血模型?

其实除了常见的充血模型、贫血模型,还有不常用的失血模型、胀血模型,区别如下:

  • 失血模型:只包含Getter/Setter的纯数据类,一般不会有这种设计
  • 贫血模型:包含模型属性、Getter/Setter与非持久化的原子领域逻辑,持久化逻辑放在业务层(如Service类)
  • 充血模型:比贫血模型多了持久化操作与绝大多数业务逻辑,实例化时会拿到很多不一定需要的关联模型
  • 胀血模型:只有领域对象与DAO两层,在领域逻辑上封装事务

基于现有的Spring框架,以及个人以往的代码编写经验,在代码落地层面还是以贫血模型进行较恰当。

2、放应用服务还是领域服务?

应用服务在应用层,领域服务在领域层,我怎么知道业务代码该放哪里?

应用服务的作用:

  • 负责展现层与领域层之间的协调,协调业务对象来执行特定的应用程序任务(编排业务)
  • 放相对灵活的代码逻辑,易于编排
  • 操作粒度较大,事务管理在此处理

领域服务的作用:

  • 负责表达业务概念,业务状态信息以及业务规则,是业务软件的核心
  • 放相对原子性的核心代码,封装性与复用性强
  • 操作粒度较细,不管理事务,领域模型不应该意识到事务的存在

其实,难点在于识别业务代码,考验我们对业务的理解程度与思考程度,如果可以显然预料到未来会发生明显的变化,则应该在设计之初更灵活地设计好;如果对未来的变化把握并不清晰或不确定,满足当前业务需求即可。

我们无法避免过度设计还是设计不足,但如果架构合理,代码清晰,改起来成本不会特别大。这里提倡开发者尽量多与领域专家(业务人员或产品经理)沟通,以把握代码未来的走向。

3、特殊代码如何归类?

除了常规的简单业务代码,还涉及到复杂业务代码拆分到不同类的问题,最典型的是运用设计模式。

  • 工厂模式:根据不同条件生成相应对象,常见领域工厂、领域服务、应用服务内
  • 策略模式:根据不同条件执行相应逻辑,定义一个策略接口和多个策略实现,常见领域服务、应用服务内
  • 观察者模式:使用发布/订阅模式代替,可运用基础设施层的SpringEvent来解耦代码
  • 责任链模式:拆分复杂业务逻辑到各个责任链类执行,常见领域服务、应用服务内

原则上,核心逻辑在哪一层拆就放在哪一层,避免代码散落到各处。

一些经验

DDD领域建模三大步:划分边界、统一语言、组织模型。

DDD工程落地四大步:整合框架思想、确定划分思路、模型代码映射、特殊代码归类。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/505192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

让ChatGPT来制作Excel表格,ChatGPT实现文本和表格的相互转换

Office 三套件可以说是现代办公族必备的办公工具。其中,Excel 因为内置的计算函数、VBA 宏等高级功能又成为了非专业人士最头疼的 Office 组件。非财务专业人士,估计平常会用的 Excel 函数仅限于 SUM(), AVERAGE() 等,甚至这些都是通过界面点…

【2023 · CANN训练营第一季】应用开发深入讲解——第三章应用调试

学习资源 日志参考文档 应用开发FAQ 日志主要用于记录系统的运行过程及异常信息,帮助快速定位系统运行过程中出现的问题以及开发过程中的程序调试问题。 日志分为如下两大类: 系统类日志:系统运行产生的日志。主要包括: Contro…

shiro CVE-2016-4437 漏洞复现

shiro Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序漏洞原理 在Apache shiro的框架中,执行身份验证时提供了…

【开发者必读】如何在MyEclipse中使用内联搜索?

MyEclipse v2022.1.0正式版下载 DevStyle中的内联搜索取代了传统的Eclipse查找和替换对话框,提供了一种更加高效和非侵入性的搜索体验——一种不会中断您的开发工作流程的工具。 DevStyle是一个Eclipse插件,也包含在MyEclipse中。 选择内联搜索参数 …

C++编译器对于对象的优化

C编译器对于对象构造的优化 用临时对象生成新对象时&#xff0c; 临时对象就不产生了&#xff0c;直接构造新对象即可 class Test { public:Test(int a 10) :ma(a){cout << "Test(int)" << endl;}~Test(){cout << "~Test()" <<…

node中npm依赖安装顺序,package-lock.json文件详解

前置知识&#xff1a;需要先了解package.json 和package-lock.json的基本知识和使用方法&#xff0c;可以参考这篇文章。 npm依赖安装的逻辑和顺序可以参考这篇文章 理论看完了我们来看一下实际项目中的是啥样的&#xff0c;上面文章所讲的逻辑都会在npm install之后&#xf…

程序员面试金典16.*

文章目录 16.01 交换数字16.02单词频率16.03交点16.04 井字游戏16.05 阶乘尾数16.06 最小差16.07 最大数值16.08 整数的英文表示16.09 运算16.10 生存人数16.11 跳水板16.13 平分正方形16.14 最佳直线&#xff08;待定&#xff09;16.15珠玑妙算16.16部分排序16.17连续数列16.1…

Hadoop HDFS的API操作

客户端环境准备 hadoop的 Windows依赖文件夹&#xff0c;拷贝hadoop-3.1.0到非中文路径&#xff08;比如d:\&#xff09;。 配置HADOOP_HOME环境变量 配置Path环境变量。 不能放在包含有空格的目录下&#xff0c;cmd 输入hadoop显示此时不应有 \hadoop-3.0.0\bin\。我放在…

关于linux中防火墙的命令

文章目录 一、linux 6.5 下二、linux 7.0 下 (CentOs7.3)常用命令 三、关于端口的一些命令四、一些状况 linux不同版本防火墙是不同的&#xff0c;命令如下 一、linux 6.5 下 service iptables status ## 查看防火墙状态 service iptables start ## 开启防火墙 service iptab…

谁还在AI焦虑?

时至今日&#xff0c;人们对GPT 为首的诸多AI&#xff0c; 大有热情消退的迹象。 与2个月前相比&#xff0c;简直恍如隔世。 这也进步一部印证了“山洞隐喻” 人类始终对未知充满恐惧和焦虑。 曾经人们忧心忡忡&#xff0c;整天讨论AI&#xff0c; 取代人类工作之后&…

如何用ChatGPT做新品上市推广方案策划?

该场景对应的关键词库(28个&#xff09;&#xff1a; 品牌、产品信息、新品、成分、属性、功效、人群特征、客户分析、产品定位、核心卖点、推广策略、广告、公关、线上推广、线下活动、合作伙伴、资源整合、预算、执行计划、监测、评估、微调方案、价值主张、营销策略、热点话…

第四十七章 Unity 布局(中)

在上一章节中我给父元素Panel添加了Horizontal Layout Group组件&#xff0c;并且添加了两个Text元素。 我们发现两个Text UI 元素在水平方向上面依次放置在Panel的最上面。由于Panel的宽度为300&#xff0c;而两个Text的总宽度为 160 160 320&#xff0c;因此两个Text 超出了…

C++入门知识(下)

目录 一、内联函数 1.1内联函数的概念 1.2内联函数的使用 1.3内联函数的特性 1.4宏的优缺点 1.5C中可替代宏的技术 二、auto关键字 2.1什么是auto关键字 2.2auto简介 2.3auto的使用细则 2.4auto不能推导的场景 三、基于范围的for循环&#xff08;C11&#xff09; 3.…

大屏只用来做汇报?知道这6个应用场景,直接升职加薪!

五一假几个朋友小聚了一下&#xff0c;好久没联系了&#xff0c;现在才知道大家从事行业五花八门的。知道我从事IT行业好几年&#xff0c;他们非要让我讲讲现在异常火爆的大屏&#xff0c;说是所在企业单位都在研究这玩意儿&#xff0c;有的业务人员焦虑不已不知道如何下手&…

Lenovo m93 mini 电脑 Hackintosh 黑苹果efi引导文件

原文来源于黑果魏叔官网&#xff0c;转载需注明出处。&#xff08;下载请直接百度黑果魏叔&#xff09; 硬件型号驱动情况 主板Lenovo m93 mini 处理器Intel i5-4590T 2.20GHz (35w) 4-core/4-thread已驱动 内存8GB (2x4) DDR3 1600MHz已驱动 硬盘2.5" SSD Samsung 8…

《Linux 内核设计与实现》11. 定时器和时间管理

文章目录 内核中时间的概念节拍率&#xff1a;HZ理想的 HZ 值高 HZ 的优势高 HZ 的劣势 jiffiesjiffies 的内部表示jiffies 的回绕用户空间和 HZ 硬时钟和定时器实时时钟系统定时器 时钟中断处理程序实际时间定时器使用定时器定时器竞争条件实现定时器 延迟执行忙等待短延迟sch…

跨境商城APP开发需要注意的问题

随着全球化的趋势&#xff0c;跨境电商发展迅猛&#xff0c;越来越多的企业开始进军跨境市场。而跨境商城APP已经成为跨境电商非常重要的一部分。在开发跨境商城APP时&#xff0c;需要注意以下问题&#xff1a; 1.多语言支持 跨境商城APP需要支持不同国家和地区的语言&#x…

在基于Android以及Jetson TK平台上如何写32位的Thumb-2指令

由于Android以及Jetson TK的编译工具链中的汇编器仍然不支持大部分的32位Thumb-2指令&#xff0c;比如 add.w&#xff0c;因此我们只能通过手工写机器指令码来实现想要的指令。下面我将简单地介绍如何在ARM GCC汇编器中手工去写机器指令码。 对于GCC或Clang的汇编器&#xff0…

es6 学习笔记-1

学习视频&#xff1a;尚硅谷Web前端ES6教程&#xff0c;涵盖ES6-ES11_哔哩哔哩_bilibili 一、介绍 ES&#xff1a;全称为EcmaScript,是脚本语言的规范 ECMAScript&#xff1a; 由Ecma国际通过ECMA-262标准化的脚本程序设计语言。 es6兼容性&#xff1a;ECMAScript 6 compa…

adb logcat 保存日志文件到本地

指令 adb logcat > logcat.log例如&#xff1a;例如&#xff1a;adb logcat > D:\logcat.log 注意window中直接输入可能会出现log文件打开显示乱码问题&#xff1b; 请打开cmd检查 输入 chcp 如图 查看结果 如果不是65001 则 执行 chcp 65001 之后执行 例如&#x…