DDD你真的理解清楚了吗?怎么准确理解“值对象”
这些年,随着软件业的不断发展,软件系统开始变得越来越复杂而难于维护。这时,越来越多的开发团队开始选择实践DDD领域驱动设计。领域驱动设计是一种非常优秀的软件设计思想,它可以非常好地帮助我们梳理复杂业务,解决大规模业务系统的设计开发与更新维护。但是,领域驱动的学习成本却非常高,使得很多同学难于准确地理解DDD,更难于真正落地实际项目的设计编码。为此,我通过这一系列知识分享,让大家真正准确地理解DDD中这些晦涩的概念,特别是让大家理解最终是怎么落地到软件项目的设计开发中的。
今天,我们首先探讨的是在DDD中,让大家最头疼、最晦涩的概念——什么是“值对象”?要理解“值对象”的设计思想,首先我们先来梳理一下领域驱动的核心思想。领域驱动认为,软件的本质就是对真实世界的模拟,软件中所有业务逻辑正确与否,唯一的判定标准就是,是否与真实世界保持一致。如果一致,设计就是OK的,否则用户就会提BUG,或者变更需求。只要理解了这个本质,软件设计就简单了:我们首先理解真实世界的业务,然后将我们对业务的理解形成软件设计。
然而,这里有一个问题,那就是软件是怎么与真实世界对应的呢?这种对应体现在以下三个方面:
1)真实世界有什么事物,软件世界就有什么对象;
2)真实世界中这些事物有什么行为,软件世界这些对象就有什么方法;
3)真实世界中这些事物间有什么关系,软件世界中这些对象就有什么关联。
因此,我们在软件设计时,首先以业务场景为单位,一个一个地分析每个业务场景都有哪些领域对象,以及它们相互之间的行为与关系,形成领域模型。然后再以领域模型为核心,完成软件的设计与开发。
譬如说,我们要设计一套电子商务系统,按照业务需求会划分为很多功能,那么每个功能就是一个业务场景。譬如在“下单”这个业务场景中,在真实世界中都有哪些事物呢?首先有“订单”,可以形成订单对象;每个订单对应一个用户,但一个用户可以有多个订单,所以从订单到用户是一个“多对一”关系;一个用户有多个地址,然而一个订单只能对应一个地址……如下图,我们按照这样的步骤逐一分析领域对象和它们之间的关系。最后,我们对订单有什么操作呢?有“下单”、“支付”、“查看订单”……
这就是领域模型,订单、用户、地址、订单明细等类,都是领域对象。然而,在DDD中还要把领域对象严格区分为“实体”与“值对象”。这时,很多同学就比较晕,什么是实体?什么是值对象?为了准确理解这个地方,我们先看看《领域驱动设计》原著是怎么说的。
实体(Entity):又称为Reference Object,它具备生命周期,并且在生命周期的过程中,其形式和内容都有可能发生变化,但它的标识永远不变。也就是说,每个实体都有一个唯一的标识,用于区分真实世界中的“他”与其他人,比如用户ID。实体是有生命周期的,比如用户的注册与注销;实体中的一切都可能会变,比如小张将自己的账户转让给了李四,但账户ID是不会变的。这个唯一的标识就是主键,以这种形式的设计,用户有用户ID、账户有账户ID、订单有订单ID,就是实体的设计。很显然,这样的设计大家都能够理解,关键是“值对象”。
值对象(Value Object):在《领域驱动设计》这本书中对值对象的描述比较隐晦,但我们大致可以归纳为以下几个特征:对象声明与对象中的属性都是不变的,可以为多个对象所引用与共享(而不是复制),这就是“值对象”。该怎样来理解这几个特征呢?
譬如,在真实世界中,一个用户可以有多个订单,那么在订单查询时,每个订单就是一个订单对象,每个订单对象都有各自不同的订单ID。因此,订单的设计是实体。然而,每个订单都要引用一个用户,也就是指向一个用户对象。那么,同一个用户的多个订单指向的是谁呢?显然,不能将这个用户复制成多个对象,而是所有这几个订单都引用的是这一个对象。也就是说,这里的“用户”对象是值对象。
然而,值对象又必须是不变的,这就是说,如果“用户”是值对象,那么用户及其相关的属性都必须是不变的。但现实情况是,用户及其用户信息都是可变的,我们会对用户信息进行增删改操作。是的,在真实世界中,一切都是在变化,没有什么是不变的。那么,怎么来理解值对象的不变性呢?
在DDD的领域建模中,除了有领域模型以外,还有一个非常重要的设计叫“限界上下文”。领域模型是描述的真实世界,但真实世界又是无限大的,那么领域模型该如何描述真实世界呢?DDD将一个复杂系统的业务划分成很多个限界上下文,然后对每个限界上下文中进行领域建模。这样,每个领域模型都有各自的边界,我只是在这个边界中描述我的业务。这样的设计,就将纷繁复杂的世界“分而治之”,对每个领域模型的分析就变得简单了。
譬如在以上案例中,可以首先将对用户及其用户档案的管理划分成一个上下文,叫“用户上下文”。在这个上下文中,用户及其属性是可变的,要进行增删改的操作,因此“用户”对象的设计是实体。然而,在另一个“订单上下文”中,订单是实体,要对其进行增删改,但订单引用的“用户”以及“地址”就是值对象。也就是说,在订单上下文中,“用户”以及“地址”是只读的,仅仅用于订单对象的引用,而不会对其进行增删改操作。这样,“用户”以及“地址”作为值对象,就体现了它的只读与不变性。
此外,站在开发的角度来说,领域模型最终要落实到软件开发。如果采用微服务的设计,就会将“用户上下文”与“订单上下文”划分为用户微服务与订单微服务。在划分微服务的同时,也会划分数据库,用户微服务有用户数据库,订单微服务有订单数据库。有了这样的设计,用户表必然是设计在用户数据库中,而不是在订单数据库中。当订单微服务要查询订单时,通过调用用户微服务的接口获得订单相关的用户对象。然而,这些用户对象只存在于订单微服务的内存中,是只读的,而不会存储在订单数据库中去增删改。这就是“值对象”的概念及其设计实现。
总而言之,DDD的实体在本上下文中是可读可写的,而值对象是只读的。在一个上下文中的核心业务是对实体的增删改,而值对象是与实体相关联的,仅仅用于实体的引用与查询的对象。值对象的特性是只读,但在实际项目中的表现有2种形式:
1) 对其它微服务中对象的引用,需要调用其它微服务的接口获得,数据不在我本地的数据库中,仅仅存在于内存中,并且不能修改,只能查询;
2) 一些类似类型、种类、类别的字典数据,虽然数据存储在本地数据库中,但没有增删改的功能,只有查询与引用。例如会员等级、积分规则、支付方式,等等。
(待续)
DDD你真的理解清楚了吗?怎么准确理解“值对象”的更多相关文章
- DDD 领域驱动设计-“臆想”中的实体和值对象
其他博文: DDD 领域驱动设计-三个问题思考实体和值对象 DDD 领域驱动设计-三个问题思考实体和值对象(续) 以下内容属于博主"臆想",如有不当,请别当真. 扯淡开始: 诺兰的 ...
- [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)
一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...
- 从壹开始微服务 [ DDD ] 之八 ║剪不断理还乱的 值对象和Dto
缘起 哈喽大家周四好,时间是过的真快,这几天一直忙着在公司的项目,然后带带新人,眼看这周要过去了,还是要抽出时间学习学习,这些天看到群里的小伙伴也都在忙着新学习,还是很开心的,至少当时的初衷已经达到了 ...
- DDD实战与进阶 - 值对象
目录 DDD实战与进阶 - 值对象 概述 何为值对象 怎么运用值对象 来看一个例子 值对象的持久化 总结 DDD实战与进阶 - 值对象 概述 作为领域驱动设计战术模式中最为核心的一个部分-值对象.一直 ...
- 如何一步一步用DDD设计一个电商网站(九)—— 小心陷入值对象持久化的坑
阅读目录 前言 场景1的思考 场景2的思考 避坑方式 实践 结语 一.前言 在上一篇中(如何一步一步用DDD设计一个电商网站(八)—— 会员价的集成),有一行注释的代码: public interfa ...
- DDD 领域驱动设计-三个问题思考实体和值对象(续)
上一篇:DDD 领域驱动设计-三个问题思考实体和值对象 说实话,整理现在这一篇博文的想法,在上一篇发布出来的时候就有了,但到现在才动起笔来,而且写之前又反复读了上一篇博文的内容及评论,然后去收集资料, ...
- DDD 领域驱动设计-三个问题思考实体和值对象
消息场景:用户 A 发送一个消息给用户 B,用户 B 回复一个消息给用户 A... 现有设计:消息设计为实体并为聚合根,发件人.收件人设计为值对象. 三个问题: 实体最重要的特性是什么? Messag ...
- DDD 领域驱动设计-Value Object(值对象)如何使用 EF 进行正确映射
写在前面 首先,这篇博文是用博客园新发布的 MarkDown编辑器 编写的,这也是我第一次使用,语法也不是很熟悉,但我觉得应该会很爽,博文后面再记录下用过的感受,这边就不多说. 阅读目录: 上一篇回顾 ...
- 正确理解DTO、值对象和POCO
今天推荐的文章比较技术化也比较简单,但是对于一些初学者而言,可能也是容易搞混的概念:就是如何理解DTO.值对象和POCO之间的区别. 所谓DTO就是数据传输对象(Data Transfer Objec ...
- [0] DDD领域驱动设计(二) 之 值对象
DDD中实体对象与值对象的解释比较抽象.主要根据持续性与 ID 识别来区分. ID并非某一对象的直观自然属性,而是在分析建模之 后,赋给模型中的实体类,来达到跟踪,区别,存储目的的一个特值. 结合项目 ...
随机推荐
- 使用 vuex 和 本地存储实现永久性token存在 并且在请求拦截统一添加headers token 避免重复代码
在 vuex 仓库中设置state的token值:从本地中取值: 登录的时候调用唯一可以修改state数据的mutations方法设置token : export default new Vuex.S ...
- keycloak~token有效期与session有效期
一 refresh_token刷新access_token Keycloak会话管理中,获取到accessToken和refreshToken后,基于accessToken交换用户数据或者参与Keyc ...
- C#线性查找算法
前言 线性查找算法是一种简单的查找算法,用于在一个数组或列表中查找一个特定的元素.它从数组的第一个元素开始,逐个检查每个元素,直到找到所需的元素或搜索完整个数组.线性查找的时间复杂度为O(n),其中n ...
- CTF-CRYPTO-RSA
CTF-CRYPTO-RSA 只是个人理解,可能有不正确的地方,具体RSA算法参考:http://8.146.200.37:4100/crypto/asymmetric/rsa 1.RSA算法概述 R ...
- 【Playwright + Python】系列(九)Playwright 调用 Chrome 插件,小白也能事半功倍
哈喽,大家好,我是六哥!今天我来给大家分享一下如何使用playwight调用chrome插件,面向对象为功能测试及零基础小白,我尽量用大白话的方式举例讲解,力求所有人都能看懂,建议大家先收藏,以免后面 ...
- 模态内重叠优化,简单有效的CLIP微调方法 | BMVC'24 Oral
来源:晓飞的算法工程笔记 公众号,转载请注明出处 论文: CLIP Adaptation by Intra-modal Overlap Reduction 论文地址:https://arxiv.org ...
- 鸿蒙Navigation知识点详解
Navigation是路由导航的根视图容器,一般作为页面(@Entry)的根容器,包括单栏(Stack).分栏(Split)和自适应(Auto)三种显示模式.Navigation组件适用于模块内和跨模 ...
- 2023NOIP A层联测16 T3 货物运输
2023NOIP A层联测16 T3 货物运输 题目描述说这是一个仙人掌图,通常将问题转换为环和树的问题在使用圆方树来解决. 树解法 令 \(a_i=s_i-\frac{\sum s_i}{n}\) ...
- CF2023C C+K+S 题解
题面 给您两个强联通的 \(^{\dagger}\) 有向图,每个图都有精确的 \(n\) 个顶点,但可能有不同数量的边.仔细观察后,您发现了一个重要特征--这些图中任何一个环的长度都能被 \(k\) ...
- [ATCoder] Cyclic GCDs - 神圣的数学题
Cyclic GCDs 题面 [题目描述] 给定一个长为 \(N\) 的序列 \(a_1,a_2,\dots,a_N\). 设一个置换 \(p\) 的价值 \(f(p)\) 为每个轮换中最小的 \(a ...