7实体与值对象 #

本系列包含以下文章:

  1. DDD入门
  2. DDD概念大白话
  3. 战略设计
  4. 代码工程结构
  5. 请求处理流程
  6. 聚合根与资源库
  7. 实体与值对象(本文)
  8. 应用服务与领域服务
  9. 领域事件
  10. CQRS

案例项目介绍 #

既然DDD是“领域”驱动,那么我们便不能抛开业务而只讲技术,为此让我们先从业务上了解一下贯穿本文章系列的案例项目 —— 码如云(不是马云,也不是码云)。如你已经在本系列的其他文章中了解过该案例,可跳过。

码如云是一个基于二维码的一物一码管理平台,可以为每一件“物品”生成一个二维码,并以该二维码为入口展开对“物品”的相关操作,典型的应用场景包括固定资产管理、设备巡检以及物品标签等。

在使用码如云时,首先需要创建一个应用(App),一个应用包含了多个页面(Page),也可称为表单,一个页面又可以包含多个控件(Control),比如单选框控件。应用创建好后,可在应用下创建多个实例(QR)用于表示被管理的对象(比如机器设备)。每个实例均对应一个二维码,手机扫码便可对实例进行相应操作,比如查看实例相关信息或者填写页面表单等,对表单的一次填写称为提交(Submission);更多概念请参考码如云术语。

在技术上,码如云是一个无代码平台,包含了表单引擎、审批流程和数据报表等多个功能模块。码如云全程采用DDD完成开发,其后端技术栈主要有Java、Spring Boot和MongoDB等。

码如云的源代码是开源的,可以通过以下方式访问:

码如云源代码:GitHub - mryqr-com/mry-backend: 本代码库为码如云后端代码。码如云是一个基于二维码的一物一码管理平台,可以为每一件“物品”生成一个二维码,手机扫码即可查看物品信息并发起相关业务操作,操作内容可由你自己定义,典型的应用场景包括固定资产管理、设备巡检以及物品标签等。在技术上,码如云是一个无代码平台,全程采用DDD、整洁架构和事件驱动架构思想完成开发。

实体与值对象 #

在本系列的上一篇聚合根与资源库中,我们讲到了聚合根的设计与实现,事实上聚合根本身即是一种实体(Entity),在本文中我们将对实体以及与之相对立的值对象(Value Object)做展开讲解。

在对聚合根的深入分析中,我们发现其中存在两种类型的对象,一种是具有生命周期的对象(比如成员(Member)),另一种是只起描述作用的对象(比如地址(Address)),前者称为实体,后者称为值对象,充分认识这两种对象之间的区别,对DDD落地有着举足轻重的作用。我们希望达到的目的是,将尽量多的概念建模为值对象,因为值对象比实体更加简单。

实体的生命周期意味着实体具有从产生到消亡的整个过程,这个过程往往比较漫长。比如,在码如云中,成员(Member)对象的生命周期可能超过几年甚至几十年的时间。相比之下,值对象不存在生命周期可言。为了讲解更加直观,让我们来分别看看值对象和实体的例子。在码如云中,地址(Address)即是一个值对象:

//Address@Value
@Builder
@AllArgsConstructor(access = PRIVATE)
public class Address {private final String province; //省份private final String city; //城市private final String district; //区县private final String address; //详细地址//......此处省略更多代码}   

源码出处:com/mryqr/core/common/domain/Address.java

聚合根成员(Member)则是一个实体对象:

//Member@Getter
@Document(MEMBER_COLLECTION)
@TypeAlias(MEMBER_COLLECTION)
@NoArgsConstructor(access = PRIVATE)
public class Member extends AggregateRoot {private String name;//名字private Role role;//角色private String mobile;//手机号private String email;//邮箱private IdentityCard identityCard;//身份证//...此处省略更多代码}

源码出处:com/mryqr/core/member/domain/Member.java

咋一看,实体和值对象似乎没有什么区别,都是Java对象而已,但事实上,实体和值对象在唯一标识、相等性和可变性等方面均存在很大的区别。

唯一标识 #

值对象的“描述性作用”也意味着它无需唯一标识(即ID)即可完成其使命,而实体则恰恰相反。在本例中,值对象Address没有ID,而实体Member的唯一标识则存在于其父类AggregateRootid字段中:

//AggregateRoot@Getter
public abstract class AggregateRoot implements Identified {private String id;//聚合根IDprivate String tenantId;//租户ID//...此处省略更多代码}

源码出处:com/mryqr/core/common/domain/AggregateRoot.java

更多关于AggregateRoot的内容,请参考本系列的聚合根与资源库一文。

在DDD中,所有的聚合根都是实体对象,但并不是所有的实体都是聚合根,不过从实践上来看了,绝大多数的实体对象都是聚合根。因此,在DDD项目中最常见的情况是:作为实体对象的聚合根包含了大量的值对象。

对于聚合根而言,由于已经是领域模型中的顶层对象,其唯一标识应该是全局唯一的;而对于聚合根下的其他实体而言,由于其作用范围被限制在了聚合根内部,因此对应的唯一标识在聚合根下唯一即可。比如,在码如云中,一个应用(App)包含了多个页面(Page),App是聚合根,PageApp下的实体,App的ID必须全局唯一,而Page的ID在其所属的App下唯一即可。

实体的唯一标识可以有多种方式生成,有些业务数据天然即是唯一标识,比如对于人员来说,身份证号即可直接用于唯一标识。不过需要注意的是,只有那些不变的业务字段才能用于唯一标识,否则,当这些业务字段发生更新时,所有引用它的地方都需要做相应更新。更多的时候,我们建议采用一个无业务含义的ID作为唯一标识,比如UUID或者通过雪花算法生成的ID等,又由于UUID的无序性在大数据量场景下可能存在性能问题,因此我们更偏向于雪花算法ID。

有些技术框架可以设置延后对实体ID的生成,比如Hibernate和数据库自增ID等,在DDD中,我们强烈建议不要采用这些方式,因为这些方式所创建出来的实体对象直到保存到数据库的最后一刻都是非法的,更好的方式是在新建实体时即为之设置ID。

在码如云中,我们通过雪花算法为聚合根生成ID,并且在构造函数中完成了对ID的赋值,以达到在新建时即为ID赋值的目的。比如,在Member对象的其中一个构造函数中,我们调用了newMemberId()为新成员生成ID:

//Member//创建Member
public Member(String name, String mobile, User user) {super(newMemberId(), user);this.name = name;this.mobile = mobile;//...此处省略更多代码
}//通过雪花算法生成成员ID
public static String newMemberId() {return "MBR" + newSnowflakeId();
}

源码出处:com/mryqr/core/member/domain/Member.java

有时,为了一些纯技术上原因,我们需要为值对象设置ID。比如,如果采用通过ORM框架持久化租户(Tenant),则需要将Tenant中的发票地址(invoiceAddress)保存到一张单独的数据库表中,由于数据库表之间需要有外键关联,因此需要将Address继承自一个层超类IdentifiedValueObject,在IdentifiedValueObject中包含有用于数据库表外键关联的id字段。

此时的Tenant实现如下:

//Tenant@Getter
@Document(TENANT_COLLECTION)
@TypeAlias(TENANT_COLLECTION)
@NoArgsConstructor(access = PRIVATE)
public class Tenant extends AggregateRoot {private String name;//租户名称private InvoiceTitle invoiceTitle;//发票抬头private Address invoiceAddress;//发票地址//...此处省略更多代码}

源码出处:com/mryqr/core/tenant/domain/Tenant.java

层超类IdentifiedValueObject实现如下:

//IdentifiedValueObjectpublic abstract class IdentifiedValueObject {private String id;
}

此时的Address继承自IdentifiedValueObject

//Addresspublic class Address extends IdentifiedValueObject {private final String province;//...此处省略更多代码}

需要强调的是,以上“为值对象设置ID”的做法仅仅是一种技术上的实践,不能将其与业务相混淆,为此我们引入了一个层超类IdentifiedValueObject将与技术相关的内容作为一个单独的关注点来处理,从而实现了技术与业务的隔离。不过,在码如云,由于我们采用了MongoDB,从而避开了ORM,因此不存在本例中的问题。

相等性判断 #

实体对象通过ID进行相等性判断,而值对象通过其自身携带的属性进行相等性判断。举个例子,对于一对双胞胎而言,每人都是一个实体对象,由于二人的身份证号(唯一标识)是不同的,因此无论二人长得多么的相像,均不能认为是同一个人;相反,对于其中某一人来说,哪怕是整容到面目全非,也依然是同一个人,因为其ID始终没变。又比如,对于常见的值对象货币(Currency)而言,其价值通过其面值决定,因此一张刚从印钞厂出来的崭新百元大钞和一张沾满了细菌的百元纸币是可以等值互换的,因为它们所携带的面值是相同的。

在编码实践上,最显著的区别是值对象需要实现equals()hashCode()方法,而实体则不需要。在码如云中,我们通过Lombok为值对象自动生成equals()hashCode()方法,比如对于存储身份证信息的IdentityCard,其实现为:

//IdentityCard@Value
@Builder
@AllArgsConstructor(access = PRIVATE)
public class IdentityCard {private String number;private String name;
}

源码出处:com/mryqr/core/member/domain/IdentityCard.java

其中的@Value注解隐式地为IdentityCard对象实现了equals()hashCode()方法。

可变性 #

实体和值对象的另一个区别是:实体对象是可变的(Mutable),而值对象是不可变的(Immutable)。对于实体对象而言,我们可以通过调用其上的方法直接更改其状态;而对于值对象而言,如果需要改变其状态,我们只能创建一个新的值对象,然后在新对象中包含改变后的状态。

对实体对象的直接状态变更比较好理解,这里重点讲一讲对值对象的不可变性的编码处理。对于值对象Address,如果我们需要修改其下的详细地址,具体的实现如下:

//Address//修改详细地址
public Address changeTo(String detailAddress) {return Address.builder().province(this.province).city(this.city).district(this.district).address(detailAddress).build();
}

源码出处:com/mryqr/core/common/domain/Address.java

这里,我们并未直接修改Address对象的address属性,而是新建了一个Address对象,然后将无需修改的字段(比如provice)原封不动地拷贝到新对象中,而将需要修改的字段(address)在新对象中设置为传入的最新值,最后返回这个新建的对象。

不可变性要求值对象必须满足以下约束:

  • 不能有共有的setter方法,否则外界可以直接修改其内部的状态
  • 不能有导致内部状态变化的共有方法

值对象的好处 #

本文一开始就提到我们应该将尽量多的对象建模为值对象,因为它比实体更加的简单,事实上值对象有多种好处。

首先,因为值对象是不可变的,所以不可变对象所拥有的好处值对象都有,比如使得对程序的调试和推理更加的简单,线程安全等。

其次,值对象作为一个概念上的整体(Conceptual Whole),它将与之相关的业务逻辑包含在其内部,不仅体现了内聚性,也增加了业务表达力,而这正是DDD所提倡的,比如对于本文中的Address,你是希望直接操作4个原始字段(provincecitydistrictaddress)呢,还是操作一个Address对象呢?

另外,值对象由于也包含了业务逻辑,因此可以完成自我验证,这样无论何时我们拿到一个值对象时,都可以相信这是一个合法的对象,而不用在值对象之外再做验证。

例如,在码如云中,定位信息被存放在Geopoint值对象中:

@Value
@Builder
@AllArgsConstructor(access = PRIVATE)
public class Geopoint {private static final float EARTH_RADIUS_METERS = 6371000;private final Float longitude;//经度private final Float latitude;//纬度public float distanceFrom(Geopoint that) {return distanceBetween(this.longitude, this.latitude, that.longitude, that.latitude);}private float distanceBetween(float lng1, float lat1, float lng2, float lat2) {double dLat = Math.toRadians(lat2 - lat1);double dLng = Math.toRadians(lng2 - lng1);double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *Math.sin(dLng / 2) * Math.sin(dLng / 2);return (float) (EARTH_RADIUS_METERS * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)));}public boolean isPositioned() {return longitude != null && latitude != null;}
}

源码出处:com/mryqr/core/common/domain/Geopoint.java

可以看到,Geopoint将经度longitude和纬度latitude封装在一起,成为一个概念上的整体。外部调用方无需单独处理经度和纬度数据,而是直接通过这个整体性的Geopoint对象即可完成对定位信息的操作。此外,distanceFrom()distanceBetween都是包含业务逻辑的方法,符合“行为饱满的领域对象”原则。再则,通过isPositioned()方法使得Geopoint可以自行完成业务验证。

角色可变 #

实体和值对象的划分并不是固定不变的,而是根据其所处的限界上下文决定的。一个概念在一个上下文中是一个实体对象,但是在另外的上下文中则可能是一个值对象。比如,对于上文中的货币Currency,在日常的的交易活动中,货币很明显应该被建模为一个值对象,因为在对其抽象之后我们忽略了货币的颜色,编号,新旧程度等属性,而只关注其面值。但是,如果哪天央行要做一个系统来管理每一张货币(比如对每张货币进行位置跟踪),那么则需要根据货币的编号进行管理,此时的货币则变成了一个实体对象。

总结 #

实体和值对象是领域对象中的两种不同类型的对象,它们在唯一标识、相等性和可变性等方面均存在不同。在DDD项目中,所有的聚合根均是实体,但是在实际建模过程中,由于值对象在不变性等方面的好处,我们应该尽量将业务概念建模为值对象。在下文应用服务与领域服务中,我们将对应用服务和领域服务做详细讲解。

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

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

相关文章

省时省力!推荐几款国内高效办公软件

随着信息技术的快速发展,办公室的工作越来越依赖于电脑和互联网。而高效的办公软件也成为了办公室必不可少的工具。今天我们来分享一些国内的高效办公软件品牌,希望对大家有所帮助。 1、J2L3x J2L3x 是一款专为团队通信而设计的工具,旨在将…

C语言自定义类型讲解:结构体,枚举,联合(2)

🐵本篇文章将会对位段、枚举和联合的相关知识进行讲解 1. 位段📚 1.1 什么是位段 位段的声明和结构体类似,但是有两点不同: 1.位段的成员必须是int,unsigned int,signed int (C99之后也可以是其他成员&am…

【C语言】通讯录

目录 一、关于通讯录 二、代码逻辑 三、通讯录实现 1.菜单设计 2.逻辑主要功能设计 3.增加联系人功能实现 4.显示全部联系人信息 5.删除联系人 6.查找联系人 7.修改联系人信息 8.对联系人进行排序 9.一键清空所有联系人 四、完整源码 test.c contact.c contact.…

Smart UI Web 16.0.1 WebComponents htmlelements Crack

Javascript Web 组件库 Smart UI Web 组件库是您构建令人惊叹的 Web 应用程序所需的唯一套件。它包含 70 多个快速且专业设计的 UI 组件,可在单个包中实现美观且始终现代的 Web 应用程序。 具有高级功能的即用型Javascript 组件。只需几行代码即可使用数据网格、甘特…

Docker 容器编排

是什么 Docker-Compose是 Docker 官方的开源项目,负责实现对Docker容器集群的快速编排。 Compose 是 Docker 公司推出的一个工具软件,可以管理多个 Docker 容器组成一个应用。你需要定义一个 YAML 格式的配置文件docker-compose.yml,写好多个…

什么是关系模型? 关系模型的基本概念

关系模型由IBM公司研究员Edgar Frank Codd于1970年发表的论文中提出,经过多年的发展,已经成为目前最常用、最重要的模型之一。 在关系模型中有一些基本的概念,具体如下。 (1)关系(Relation)。关系一词与数学领域有关,它是集合基…

干货 | 基于在线监控数据的非现场监管问题识别模型研究

以下内容整理自2023年夏季学期大数据能力提升项目《大数据实践课》同学们所做的期末答辩汇报。 我们汇报的题目是基于在线监控数据的非现场监管问题识别模型研究,我们的汇报将从五个部分展开。首先是项目背景说明,该项目是为了遏制企业逃避监管行为的发生…

(自学)黑客技术——网络安全

如果你想自学网络安全,首先你必须了解什么是网络安全!,什么是黑客!! 1.无论网络、Web、移动、桌面、云等哪个领域,都有攻与防两面性,例如 Web 安全技术,既有 Web 渗透2.也有 Web 防…

精彩回顾 | 迪捷软件亮相2023世界智能网联汽车大会

2023年9月24日,2023世界智能网联汽车大会(以下简称大会)在北京市圆满落幕。迪捷软件北京参展之行圆满收官。 本次大会由工业和信息化部、公安部、交通运输部、中国科学技术协会、北京市人民政府联合主办,是我国首个经国务院批准的…

希望杯、希望数学系列竞赛辨析和希望数学超1G的真题和学习资源

中国的中小学数学竞赛种类非常多,但是说到全国性的数学竞赛,影响力最大的之一就是“希望杯”,在2017年国家喊停学科竞赛后,“希望杯”逐步停止了,但是鉴于希望杯的巨大影响力,以及背后的利益纠葛&#xff0…

域名备案流程(个人备案,腾讯云 / 阿里云)

文章目录 1.网站备案的目的2.备案准备的材料2.1 网站域名2.2 云资源或备案授权码2.3 电子材料 3.首次个人备案准备的材料3.1 主体相关3.2 域名相关3.3 网站相关3.4 网站服务相关3.5 变更相关 4.个人备案流程4.1 登录系统4.2 填写备案信息🍀 填写备案省份&#x1f34…

2023 “华为杯” 中国研究生数学建模竞赛(E题)深度剖析|数学建模完整代码+建模过程全解全析

​ 问题一 血肿扩张风险相关因素探索建模 思路: 根据题目要求,首先需要判断每个患者是否发生了血肿扩张事件。根据定义,如果后续检查的血肿体积比首次检查增加≥6 mL或≥33%,则判断为发生了血肿扩张。 具体判断步骤: (1) 从表1中提取每个患者的入院首次影像检查…

十大直线导轨品牌

在现如今的制造业领域中,直线导轨作为重要的传动元件,广泛应用于各种机械装置中,以下是十个在直线导轨领域具有优秀表现的品牌,我们一起来看看: 1、日本THK,致力于开发、生产并且销售LM滚动导轨、滚珠花键、…

设计模式篇---桥接模式

文章目录 概念结构实例总结 概念 桥接模式:将抽象部分与它的实现部分解耦,使得两者都能够独立变化。 毛笔和蜡笔都属于画笔,假设需要有大、中、小三种型号的画笔,绘画出12种颜色,蜡笔需要3*1236支,毛笔需要…

什么是JavaScript中的IIFE(Immediately Invoked Function Expression)?它的作用是什么?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ JavaScript中的IIFE⭐ 示例⭐ 写在最后 ⭐ 专栏简介 前端入门之旅:探索Web开发的奇妙世界 欢迎来到前端入门之旅!感兴趣的可以订阅本专栏哦!这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们…

【JavaEE】多线程(三)

多线程(三) 续上文,多线程(二),我们已经讲了 创建线程Thread的一些重要的属性和方法 那么接下来,我们继续来体会了解多线程吧~ 文章目录 多线程(三)线程启动 startsta…

Git学习笔记4

GitHub是目前最火的开源项目代码托管平台。它是基于web的Git仓库,提供公有仓库和私有仓库,但私有仓库是需要付费的。 到Github上找类似的项目软件。 GitLab可以创建免费的私有仓库。 GitLab是利用 Ruby开发的一个开源的版本管理系统,实现一个…

【搭建私人图床】使用LightPicture开源搭建图片管理系统并远程访问

文章目录 1.前言2. Lightpicture网站搭建2.1. Lightpicture下载和安装2.2. Lightpicture网页测试2.3.cpolar的安装和注册 3.本地网页发布3.1.Cpolar云端设置3.2.Cpolar本地设置 4.公网访问测试5.结语 1.前言 现在的手机越来越先进,功能也越来越多,而手机…

基于STM32+华为云IOT设计的智能车库管理系统

一、项目介绍 随着城市化进程和汽车拥有率的不断提高,停车难的问题也日益凸显。在城市中,停车场是一个非常重要的基础设施,但是传统的停车场管理方式存在很多问题,比如车位难以管理、停车费用不透明等。为了解决这些问题&#xf…

时间轮算法

思考 假如现在有个任务需要3s后执行,你会如何实现? 线程实现:让线程休眠3s 如果存在大量任务时,每个任务都需要一个单独的线程,那这个方案的消耗是极其巨大的,那么如何实现高效的调度呢? 时…