DDD领域驱动设计记账软件实战教程–划分领域和聚合
本文章为实战教程,主要内容包括:
- 从零使用DDD构建一个记账软件
你是否有以下疑问?
- 你是一个DDD软件开发人员,但是只是按照现有的项目去写,如果自己搭建一个新项目就无从下手了?
- 你是一个传统软件开发人员,想使用DDD重构软件,却不知道如何入手?
- 你看过很多DDD的文章,了解DDD概念,却没有一个从头到尾的完整教程,无法把碎片化的知识串起来?
本文章带你从头构建一个完整的DDD项目。由点到面的串联起来所有的知识。达成一个完整的知识图谱。
事件风暴
首先需要进行事件风暴
。整理出来所有的命令
、业务流
和事件
。
- 命令:可以简单理解为一些行为。这些命令可以映射成代码模型中的一个个方法。
- 业务流:可以按照场景进行分析业务流,比如对于记账软件来说:记账场景、查账场景就是两个不同的场景,可能有不同的业务流。业务流可以映射成代码模型中的接口。
- 事件:可以简单理解成上面的命令产生的一些事件。会有一些
监听者
异步监听这些事件并实现一些逻辑。
我们来分析一些记账场景会有哪些业务流。
首先用户需要注册登陆。因此我们可以识别出注册业务流
和登陆业务流
。继而识别出注册命令
和登陆命令
。
对于登陆业务流中来说,除了登陆命令,还可以有查询用户、校验用户信息、生成token等命令。这些命令共同完成了一次登陆业务流。
此外,还会产生对应的事件。比如用户已注册事件
、已登陆事件
等。
对于已注册事件,我们可以发送注册成功的消息提醒。还可以参与注册活动等。
对于已登陆事件,我们可以写入登陆成功日志,发送登陆成功消息提醒等。
登陆成功以后,首先需要创建一个账本
。接下来可以更新账本信息。更新预算。设置角色信息。然后就可以添加收支记录在账本上了。
DDD领域其实是现实世界的映射。这里我们要思考现实世界的记账是怎么样的。
现实世界需要一个账本、需要记录收支信息、账本的最上方可能会写上本月的预算。记录收支信息的时候会写上分类、角色、金额等信息。
在这里我仅仅分析了记账场景的业务流。其实还有查账场景的业务流。此外还有一些其他的通用业务流。比如监控,数据统计等。
大家可以自己分析一下将结果发到评论区。我会一一回复的。
纸上得来终觉浅,绝知此事要躬行
领域建模
当我们分析完所有的命令
、事件
以后。我们就可以根据上面分析出来的这些进行领域建模
了。通过这些我们可以分析出领域对象。
比如,登陆注册行为是围绕用户领域对象
来完成的行为。写入登陆日志、登陆日志事件是围绕用户日志领域对象
的行为。
此外,我们还可以从上面的分析中找到账本
、预算
、角色
和收支记录
这几个领域对象。
如下图:
领域对象也分三种类型
- 聚合根:聚合的入口,操作聚合下所有的实体和值对象都需要通过聚合根来操作
- 实体:具有唯一ID的实体,使用
充血模型
实现,也就是要包含属性和方法。所有对于实体的操作都应该放到这个实体中。可以映射为数据库的一个表。 - 值对象:值对象分为简单值对象和复杂值对象。值对象和实体最大的区别在于
可变性
,值对象不能单独存在
,只能作为实体的一部分存在。同样的,值对象不支持变更,如果需要变更则是整个值对象删除再创建一个新的。- 简单值对象:基本类型的属性可以视为简单值对象。
- 复杂值对象:引用其他类(自定义的类)作为属性的值,可以视为复杂值对象。
接下来我们就可以分析出上面的领域对象哪些是实体
,哪些是值对象
了。
实体:
- 用户实体:用户实体包括用户信息和用户的一些操作。这里可以将最新的一个登陆日志作为值对象放到用户实体中。而不是将所有的登陆日志都作为值对象放到用户实体中,因为那样的话太庞大了。
- 用户登陆日志实体:存储登陆日志的信息,虽然从用户的角度来看,登陆日志不能单独存在。但是如果从数据分析的角度看,登陆日志则可以单独存在了。因此可以作为一个实体存在。
- 账本实体:账本作为一个现实世界记账必须的物品,自然也要作为一个实体存在,有账本ID保证唯一性。并且可以修改账本的部分信息。
- 收支记录实体:每一个收支记录都是账本上的一条信息,都是可以独立修改的个体。比如修改某一个记录的金额等。所以收支记录也作为一个实体存在。
值对象:
- 预算值对象:预算信息是依赖于账本的一个信息,修改预算就是修改预算这个整体。因此预算作为账本的一个值对象存在。
- 角色值对象:多个人记账,每个人记账的时候的角色都是不一样的,修改角色也是修改角色这个整体,角色离开账本以后就没有存在的意义了,因此角色也作为值对象存在。
聚合建模
经过上面的步骤,我们已经找到了我们需要的实体
和值对象
。接下来我们可以根据他们之间的关系,来划分成一个个聚合
了。
如果把实体
理解成一个个的人,那么聚合
就相当于部落
、家族
等等。相近的一些人聚集在一起,就变成了一个聚合。
变成聚合以后,当对外沟通
的时候,需要一个话事人
,也就是聚合根
。
首先我们要有用户聚合,用户聚合来管理用户信息。用户实体就是用户聚合的聚合根。
其次,用户登陆日志也作为一个单独的聚合存在。可以思考一下。
✅ 1. 领域含义:登录日志是用户行为的“历史轨迹”,而非用户本身的组成部分
- 用户(User)是一个业务主语,它的职责是认证、权限、信息管理
- 登录日志是用户行为产生的事实事件,它与用户的生命周期边界无关
✅ 2. 技术设计上:日志数据量大、写入频繁、读取模式完全不同
特性 | 用户聚合(User) | 登录日志(LoginLog) |
---|---|---|
写入频率 | 中低(修改信息、改密码) | 高(每次登录都写) |
数据量 | 小(一个用户一条) | 非常大(每用户数百/数千条) |
删除时是否联动 | 删除用户可保留日志 | 有审计/合规要求时不能删除 |
查询模式 | 通过用户 ID 查询 | 分页查询 / 最近10条等 |
这说明 LoginLog 应该用独立表、独立聚合、独立读写模型管理。
✅ 3. 聚合的职责原则(Evans 原则)
一个聚合应尽可能小,保持一致性边界清晰,仅包含保护不变式所必须的部分。
登录日志并不影响用户不变式(用户名、密码唯一性、身份验证),因此它不应该进聚合。
所以,我们把登陆日志放到一个单独的聚合中。登陆日志就是聚合根。
接下来,账本
实体、预算
和角色
值对象一起构成了账本聚合。账本作为聚合根存在。
剩下的收支记录作为一个聚合。
有了4个聚合。我们还需要划分领域。将相近的聚合放到一个领域
中。
领域可以简单理解为粗粒度的聚合。聚合中是实体和值对象。而领域中则是聚合。
我们将4个聚合划分成两个领域,分别是用户领域
和记账领域
。
总结
至此,我们完成了整个领域建模的工作,下一步就是将领域建模转换成代码建模了。
下一遍文章会将我们这次的领域建模转换成代码建模,并用spring框架来实现整个代码。
大家可以先自己将领域建模转换成代码建模,并将结果发到评论区一起讨论~
文末福利
关注我发送“MySQL知识图谱”领取完整的MySQL学习路线。
发送“电子书”即可领取价值上千的电子书资源。
发送“大厂内推”即可获取京东、美团等大厂内推信息,祝你获得高薪职位。
发送“AI”即可领取AI学习资料。
部分电子书如图所示。