领域驱动设计(DDD)之实践
xsobi 2024-12-14 15:45 1 浏览
1.简介
领域驱动设计是一个应对复杂应用系统的设计方法,它通过一系列从粗到细粒度的逻辑边界划分,从而创建系列的高内聚的领域模型,并使用与领域模型一致性的代码实现。最终,高复杂度的应用系统被划分为一个个小的低复杂度服务/功能/任务。后续文章不按照常见的战略设计+战术设计实现,只按照自己的理解来展开。
2.基本概念
领域驱动设计核心是利用业务概念创建领域模型对象最终完成系统设计,而不是数据存储出发设计系统。业务概念的来源主要是用户故事中各种业务术语。下述概念的粒度由粗到细,例如一个上下文边界中包含一个及以上的应用服务。
2.1 上下文边界
在有界上下文中,领域对象才会具有确定的语义,保证没有二义性。划分好有界上下文,就可以知道领域对象应该放在哪个上下文中实现。实际上,微服务的拆分和设计是基于有界上下文,一个上下文对应一个微服务,但是防止过度拆分带来的维护成本激增,往往会将多个上下文边界作为一个服务管理。例如,在电商领域,一个“商品”在不同的上下文中可能有不同的含义和属性。在销售上下文中,它可能包括价格、促销信息等属性;而在库存管理的上下文中,它可能包含库存数量、存储位置等属性。通过定义这两个不同的上下文边界,我们可以清晰地区分“销售商品”和“库存商品”,避免因概念混淆而导致的业务逻辑错误。
2.1.1 应用服务
应用服务通常以业务用例为粒度,每个服务对应一个独立的业务用例。应用服务主要负责对服务内部的领域服务编排。 假设我们有一个电子商务平台,该平台有一个功能是“用户提交订单”。这个业务用例可以由一个名为OrderSubmissionService的应用服务来实现。这个服务会处理用户提交订单的整个流程,包括验证用户输入的数据、创建订单、计算订单总额、检查库存、记录交易日志等。在这个过程中,OrderSubmissionService会调用领域层中的多个聚合根和实体来完成这些任务,例如调用Order聚合根的placeOrder方法,以及Product实体的decrementStock方法。
2.1.1.1 聚合
聚合由一个聚合根、多个实体、多个领域服务、多个领域事件以及一个仓储实现,这些对象在业务上高度关联,并且作为一个整体被统一管理,具有强一致性。聚合内实现高内聚的业务逻辑,如果特别复杂,则它的代码可以独立拆分为微服务。
聚合根
聚合根也是一个实体,但是封装了所在聚合内的所有领域对象的管理,并维护聚合内的强一致性。 聚合软件包的根目录,可以根据实际项目的聚合名称命名,比如权限聚合。在聚合内定义聚合根、实体和值对象以及领域服务之间的关系和边界。
领域事件
领域内产生的事件。可以关注用户故事中,“当...,则要...."这种描述。
领域服务
负责编排聚合内实体和值对象,组合出一段业务逻辑,一个聚合可以只有一个领域服务类,如果比较复杂再考虑拆分。
仓储
一个聚合对应一个仓储,负责聚合的查询和持久化。
实体
实体是最底层的领域对象之一,主要特征是:
- 具有唯一标识符,各种属性变更后标识符不变
- 包含属性和方法,使用充血模型
值对象
值对象也是最底层的领域对象之一,主要特征是:
- 不可变
- 通过对象属性值来区分而不是标识符
3.工程实现(or 分层架构)
服务内部代码的组织使用分层架构来组织,主要分为用户接入层、应用层、领域层、基础设施层来作为上述领域对象的载体。
3.1 用户接入层
负责将用户输入转换为领域对象,并调用应用服务产生结果,将结果转换为展现数据。
dto
存放web接口/soa接口的入参和出参
assembler
存放dto与领域对象的转换逻辑
facade
存放应用服务的编排代码
3.2 应用层
event
负责事件的发布和订阅
publish
subscribe
service
负责存放应用服务的业务,使用xxxCommand\xxxQuery\xxxEvent明确意图。
external
负责存放外部服务的接口
3.3 领域层
service
存放领域服务的逻辑,调用
entity
存放实体
valueObject
存放值对象
event
存放领域事件
仓储
存放仓储接口
3.4 基础设施层
负责存放rpc调用、mp配置以及事件订阅/发布接口的实现、仓储接口的数据库的实现以及缓存的实现等。
4 实践
背景
用例1:在一个设计工具中,家装设计师可以打开方案,在方案中选中想要生成台面的柜子,然后选择需要生成的台面材质样式,然后点击一键生成台面。同时,台面材质和前后挡水样式之间的约束关系有单独的配置。生成的台面块要能够恰好覆盖柜子表面。台面块和墙和柜子重叠的部分会生成后挡水,其余边生成前挡水。如果用户要求前挡水要内含,则柜子表面的轮廓要包含前挡水;如果是外扩,则前挡水可以在柜子外部。多个台面如果相邻等高,则需要合并一个台面。台面块是平面板件模型,而前后挡水则是扫掠模型。柜子和墙形成的闭合缝隙,如果面积小于100平米厘米则需要扩展台面,补上缝隙。
用例2: 生成好的台面可以根据用户的要求切割成多个台面块和多段前挡水和后挡水。
用例3: 用户选中方案中的柜子,进入脚线生成环境,选择脚线轮廓和材质样式以及脚线高度,可以使用设计工具一键生成脚线,脚线是扫掠模型。同时,首尾相接的多段脚线可以合并为一段。
任务
- 提取领域模型,并划分上下文,并再用上下文重新组织领域模型
- 完成代码的设计和组织
行动 && 结果
- 提取各种业务名词、行为还有事件 业务术语: 台面、台面块、前挡水、后挡水、内含外扩、户型、方案、墙、台面合并、台面补缝、台面切割、脚线、柜子、柜子上表面、柜子下表面、扫掠模型、放样模型、材质、轮廓样式。
- 划分上下文
- 台面上下文边界(核心)
- 台面、台面块、前挡水、后挡水、台面厚度、内含外扩、台面合并、台面补缝、台面切割、前挡水样式、台面材质、前挡水材质、后挡水材质、后挡水样式、前挡水扫掠、后挡水扫掠、台面块板件
- 脚线上下文边界(核心)
- 脚线、脚线高度、脚线材质、脚线样式、脚线扫掠
- 柜子上下文边界(支撑)
- 柜子、柜子上表面、柜子下表面
- 户型上下文边界(通用)
- 户型
- 方案上下文边界(通用)
- 方案
- 建模上下文边界(通用)
- 柜子模型、扫掠模型、放样模型
- 提取隐藏概念以及上下文
对上述的部分行为详细展开,比如台面合并:台面合并是指两个台面材质、样式一致、厚度一致且几何图形做交集,因此还有几何的上下文,包含常用的3维长方形、3维长方体、3维多边形、特殊的柱体(三维多边形沿着面的法向量拉伸形成)以及相应的相交/差/并集操作等。
同时,考虑到维护性,两个核心上下文分别使用单独的服务作为载体会导致维护成本比较高,尤其是脚线业务逻辑简单,因此考虑放在使用同一个服务作为载体。同时作为支撑脚线和台面的柜子、扫掠、板件则作为原子领域概念,组合构建出更高层的领域对象。 - 识别领域对象并分层
image.png
- 基础设施层
- 数据配置+mapper.xml以及mapper以及接口实现
- redis配置+caffeine缓存实现
- rpc client调用以及dto与do转换以及限流和熔断机制
- 动态配置实现
- 线程池实现
- 代码结构(部分)
目录: - web层
- interface
- facade
- dto
- assembler
- controller
- CountertopController
- BottomMoldingController
- application (依赖领域层)
- service
- CountertopAppService
- BottomMoldingAppService
- extern
- ModelAppService
- RenderAppService
- RoomAppService
- event (没用到可删除)
- subscribet
- publish
- domain
- countertop
- service
- CountertopDomainService
- entity
- Countertop
- CountertopBlock
- repo
- CountertopRuleRepo
- valueObject
- event(空的)
- exception
- switch (业务开关,接口与实现分离)
- bottomMolding
- domain-primitive
- util
- entity
- Cabinet
- Room
- Sweep
- Plank3d
- valueObject
- Polygon3d
- Curve3d
- infras(依赖领域层的接口)
- mq
- rocketMQ
- storage
- mysql
- 数据库A
- config (数据源配置以及事务管理器配置)
- po
- assembler
- 数据库B
- redis
- rpc
- dto
- assembler
- facade(包含rpc的各种调用,渲染、模型以及户型服务的实际调用)
基础设施层部分代码展示:
image.png
@Repository
public class DrawerClearanceConfigRepoImpl implements DrawerClearanceConfigRepo {
@Resource
private DrawerClearanceConfigMapper drawerClearanceConfigMapper;
@Resource
private DrawerClearanceConfigRelationMapper drawerClearanceConfigRelationMapper;
/**
* 通过抽屉ID列表查找配置。
*
* @param drawerId 抽屉的唯一标识ID
* @return 一个映射,将抽屉ID映射到它们的配置
*/
@Override
@Cacheable(value = "drawerClearanceConfigs", key = "#rootAccountId + '-' + #drawerId")
public DrawerClearanceConfig findConfigsByDrawerId(Long drawerId, long rootAccountId) {
final List<DrawerClearanceConfigRelationPO> drawerClearanceConfigRelationPOList
= drawerClearanceConfigRelationMapper.selectByDrawerBgIds(Collections.singletonList(drawerId), rootAccountId);
if (CollectionUtils.isEmpty(drawerClearanceConfigRelationPOList)) {
return null;
}
DrawerClearanceConfigRelationPO drawerClearanceConfigRelationPO = drawerClearanceConfigRelationPOList.get(0);
DrawerClearanceConfigPO drawerClearanceConfigPO = drawerClearanceConfigMapper.selectById(drawerClearanceConfigRelationPO.getConfigId(), rootAccountId);
return DrawerClearanceConfigConverter.toDrawerClearanceConfig(drawerClearanceConfigPO);
}
/**
* 通过配置ID删除一个配置。
*
* @param configId 配置的唯一标识ID
*/
@Override
@Transactional(transactionManager = "dcsModelTransactionManager", rollbackFor = {Exception.class}, propagation = Propagation.REQUIRED)
public void removeConfigById(long configId, long rootAccountId) {
LOGGER.message("removeConfigById").with("configId", configId).with("rootAccountId", rootAccountId).info();
drawerClearanceConfigMapper.deleteById(configId, rootAccountId);
drawerClearanceConfigRelationMapper.deleteByConfigId(configId, rootAccountId);
}
}
5 一些BP
- 聚合之间的关联要使用id
- 聚合是某个实体
- 事务的粒度就是一个聚合,多个聚合之间只能通过领域事件保证最终一致性
- 聚合设计要尽量小,如果一个实体不是根实体,但同时需要被外界直接访问到,那么这个实体不应该在这个聚合中,应该独立成新的聚合。
- 聚合根作为外界访问的入口
- 值对象和实体的equal和hash方法需要重写
- 常见的insert、select、update、delete都属于SQL语法,使用这几个词相当于和DB底层实现做了绑定。相反,我们应该把Repository当成一个中性的类似Collection的接口,使用语法如find、save、remove。
- 让Domain Service与Repository打交道,而不是让领域模型Entity与Repository打交道
原文:https://juejin.cn/post/7404739357083697188
作者:AI改变世界吗
相关推荐
- 好用的云函数!后端低代码接口开发,零基础编写API接口
-
前言在开发项目过程中,经常需要用到API接口,实现对数据库的CURD等操作。不管你是专业的PHP开发工程师,还是客户端开发工程师,或者是不懂编程但懂得数据库SQL查询,又或者是完全不太懂技术的人,通过...
- 快速上手:Windows 平台上 cURL 命令的使用方法
-
在工作流程中,为了快速验证API接口有效性,团队成员经常转向直接执行cURL命令的方法。这种做法不仅节省时间,而且促进了团队效率的提升。对于使用Windows系统的用户来说,这里有一套详细...
- 使用 Golang net/http 包:基础入门与实战
-
简介Go的net/http包是构建HTTP服务的核心库,功能强大且易于使用。它提供了基本的HTTP客户端和服务端支持,可以快速构建RESTAPI、Web应用等服务。本文将介绍ne...
- #小白接口# 使用云函数,人人都能编写和发布自己的API接口
-
你只需编写简单的云函数,就可以实现自己的业务逻辑,发布后就可以生成自己的接口给客户端调用。果创云支持对云函数进行在线接口编程,进入开放平台我的接口-在线接口编程,设计一个新接口,设计和配置好接口参...
- 极度精神分裂:我家没有墙面开关,但我虚拟出来了一系列开关
-
本内容来源于@什么值得买APP,观点仅代表作者本人|作者:iN在之前和大家说过,在iN的家里是没有墙面开关的。...
- window使用curl命令的注意事项 curl命令用法
-
cmd-使用curl命令的注意点前言最近在cmd中使用curl命令来测试restapi,发现有不少问题,这里记录一下。在cmd中使用curl命令的注意事项json不能由单引号包括起来json...
- Linux 系统curl命令使用详解 linuxctrl
-
curl是一个强大的命令行工具,用于在Linux系统中进行数据传输。它支持多种协议,包括HTTP、HTTPS、FTP等,用于下载或上传数据,执行Web请求等。curl命令的常见用法和解...
- Tornado 入门:初学者指南 tornados
-
Tornado是一个功能强大的PythonWeb框架和异步网络库。它最初是为了处理实时Web服务中的数千个同时连接而开发的。它独特的Web服务器和框架功能组合使其成为开发高性能Web...
- PHP Curl的简单使用 php curl formdata
-
本文写给刚入PHP坑不久的新手们,作为工具文档,方便用时查阅。CURL是一个非常强大的开源库,它支持很多种协议,例如,HTTP、HTTPS、FTP、TELENT等。日常开发中,我们经常会需要用到cur...
- Rust 服务器、服务和应用程序:7 Rust 中的服务器端 Web 应用简介
-
本章涵盖使用Actix提供静态网页...
- 我给 Apache 顶级项目提了个 Bug apache顶级项目有哪些
-
这篇文章记录了给Apache顶级项目-分库分表中间件ShardingSphere提交Bug的历程。说实话,这是一次比较曲折的Bug跟踪之旅。10月28日,我们在GitHub上提...
- linux文件下载、服务器交互(curl)
-
基础环境curl命令描述...
- curl简单使用 curl sh
-
1.curl--help#查看关键字2.curl-A“(添加user-agent<name>SendUser-Agent<name>toserver)”...
- 常用linux命令:curl 常用linux命令大全
-
//获取网页内容//不加任何选项使用curl时,默认会发送GET请求来获取内容到标准输出$curlhttp://www.baidu.com//输出<!DOCTYPEh...
- 三十七,Web渗透提高班之hack the box在线靶场注册及入门知识
-
一.注册hacktheboxHackTheBox是一个在线平台,允许测试您的渗透技能和代码,并与其他类似兴趣的成员交流想法和方法。它包含一些不断更新的挑战,并且模拟真实场景,其风格更倾向于CT...
- 一周热门
- 最近发表
-
- 好用的云函数!后端低代码接口开发,零基础编写API接口
- 快速上手:Windows 平台上 cURL 命令的使用方法
- 使用 Golang net/http 包:基础入门与实战
- #小白接口# 使用云函数,人人都能编写和发布自己的API接口
- 极度精神分裂:我家没有墙面开关,但我虚拟出来了一系列开关
- window使用curl命令的注意事项 curl命令用法
- Linux 系统curl命令使用详解 linuxctrl
- Tornado 入门:初学者指南 tornados
- PHP Curl的简单使用 php curl formdata
- Rust 服务器、服务和应用程序:7 Rust 中的服务器端 Web 应用简介
- 标签列表
-
- grid 设置 (58)
- 移位运算 (48)
- not specified (45)
- patch补丁 (31)
- strcat (25)
- 导航栏 (58)
- context xml (46)
- scroll (43)
- element style (30)
- dedecms模版 (53)
- vs打不开 (29)
- nmap (30)
- webgl开发 (24)
- parse (24)
- c 视频教程下载 (33)
- paddleocr (28)
- listview排序 (33)
- firebug 使用 (31)
- transactionmanager (30)
- characterencodingfilter (33)
- getmonth (34)
- commandtimeout (30)
- hibernate教程 (31)
- label换行 (33)
- curlpost (31)