文章目录
- 一 内存布局分析
- 1.1 对象的内存布局
- 1.2 实体的内存布局
- 1.3 值对象的内存布局
- 二 数据库布局分析
- 2.1 实体的数据库布局
- 2.2 值对象的数据库布局
- 2.2.1 两种方式
- 2.2.2 两种方式的比较
- 2.2.3 值对象的特点
- 相较于实体,值对象的优点,主要体现在内存和数据库布局的灵活性上。有了这种灵活性,就可以根据性能、编程方便性等因素,决定值对象的不同实现方式。其次,值对象的不变性也会带来更高的程序质量。
一 内存布局分析
- 内存布局就是程序在运行时,程序数据在内存的存储方式。
1.1 对象的内存布局
- 假如有一个员工,名字叫“张三”,出生日期是“1990年1月1日”,员工号是“1001”。他有两个技能:一个是Go语言,做过3年,达到中等水平;另一个是Java语言,做过10年,达到高级水平。他还有两段工作经历:从“2017年1月1日”到“2022年1月1日”在“ABC Inc”公司工作;从“2014年1月1日”到“2016年12月31日”在“123 Inc”工作。
- 员工对象图如下:

- 员工展开对象图

1.2 实体的内存布局
- 对于实体而言,关键原则:在同一个线程里,一个实体对象在内存空间里只能出现一次。也如果同一个实体在内存里被多个对象引用,那么这个实体必须被多个对象共享。如员工和组织的关系图:

- 或者这张情况

1.3 值对象的内存布局

- 假如员工张三和李四,都在2014年1月1日到2016年12月31日,在“123 Inc”公司工作。那么,当张三和李四同时装入内存的时候,内存布局可以有两种:一种是共享值对象,另一种不共享。


- 两种值对象的内存分布都是正确的。
- 要实现对象的共享,其实需要更复杂的编程,所以值对象在内存里多数是不共享的。比如 Java 语言里,默认情况下,字符串(String)、整数(Integer)等都是不共享的。但有时候,JVM 会“偷偷地”把相同的字符串共享,以便节省内存空间。不过,这对程序员是不可见的,我们总是应该假定字符串没有共享。
- 当值对象的体积比较大,数量比较多,共享值对象可以节省大量内存的时候,就可以采用共享的方式。这种用法实际上是一种设计模式叫做“享元”,也就是共享的单元(Lightweight)。
二 数据库布局分析
2.1 实体的数据库布局
- 在数据库表里,一个实体也只能有一条记录,不应该重复。以员工和组织关系为例。
第一种方式
id | tenent_id | org_id | num | name | gender | dob |
---|
1 | 1 | 1 | 00001 | 张三 | M | 1990-1-1 |
2 | 1 | 1 | 00002 | 李四 | F | 1995-1-1 |
第二种方式
id | tenent_id | org_id | num | name | gender | dob |
---|
1 | 1 | 1 | 00001 | 张三 | M | 1990-1-1 |
2 | 1 | 2 | 00002 | 李四 | F | 1995-1-1 |
id | tenent_id | name |
---|
1 | 1 | IT 部 |
2 | 1 | IT 部 |
2.2 值对象的数据库布局
- 值对象在数据库的存储方式比较灵活。以员工和工作经验为例。
id | tenent_id | org_id | num | name | gender | dob |
---|
1 | 1 | 1 | 00001 | 张三 | M | 1990-1-1 |
2 | 1 | 1 | 00002 | 李四 | F | 1995-1-1 |
id | tenent_id | emp_id | company | start_date | end_date |
---|
1 | 1 | 1 | 123Inc | 2014-1-1 | 2016-12-31 |
2 | 1 | 2 | 123Inc | 2014-1-1 | 2016-12-31 |
- 在内存布局里面,时间段对象占有自己的内存空间,但在数据库里,并没有单独的“时间段表”。相反,时间段的属性,也就是开始日期和束日期,是工作经验表里的两个字段。也就是说,时间段这个值对象被“嵌入”到工作经验表里了。这种“嵌入”到所属实体表的方式,正是值对象最常见的存储方式。
- 在数据库里是同一个表,在内存里却是不同的对象,这种数据库和内存的差异,称为阻抗不匹配。阻抗不匹配有多种形式,对象的嵌入式存储只是其中一种。在程序里进行数据存取的时候,就要进行转换,来消除这种不匹配。这种转换工作,是在仓库,也就是 Repository 中完成的。其实,消除这种阻抗不匹配,正是像 Hibernate 这样的 ORM 框架的主要目的之一。
2.2.1 两种方式
第一种
id | tenent_id | org_id | num | name | gender | dob |
---|
1 | 1 | 1 | 00001 | 张三 | M | 1990-1-1 |
2 | 1 | 1 | 00001 | 李四 | F | 1995-1-1 |
id | tenent_id | emp_id | provine | city | district | street | num |
---|
1 | 1 | 1 | 广东 | 深圳 | 南山区 | 文明路 | 123 |
2 | 1 | 2 | 广东 | 深圳 | 南山区 | 文明路 | 123 |
- 同一个地址信息存了两遍。这种情况对于实体是不对的,但对于值对象就没有问题,这同样是因为值对象的不变性。这种不变性在数据库操作的体现是:假如李四搬走,那么应该把和他相关的地址记录删除,再插入一条新地址,而不是改变地址表里原来的记录。
第二 种
id | tenent_id | org_id | num | name | gender | dob | addr_id |
---|
1 | 1 | 1 | 00001 | 张三 | M | 1990-1-1 | 1 |
2 | 1 | 1 | 00001 | 李四 | F | 1995-1-1 | 1 |
id | tenent_id | provine | city | district | street | num |
---|
1 | 1 | 1 | 广东 | 深圳 | 南山区 | 文明路 |
- 地址表里不再有员工的 ID(emp_id),反之,员工表里有一个地址 ID(add_id),指向地址表。
2.2.2 两种方式的比较
- 共享值对象的好处包括两点:节省存储空间、容易通过数据库查询找到住在同一个地址的人。代价是删除员工的时候不能随便删地址。如果所有使用这个地址的人都被删除,就会留下垃圾数据。所以需要有清理垃圾数据的机制。由于现在硬盘都比较便宜,所以不共享的方式反而比较常用。
2.2.3 值对象的特点
首先,值对象是不可变的,所以采用值对象就可以减少出错的几率。
其次,值对象具有不可变性,使用值对象,也可以减少并发错误。
最后,函数式编程总是假定被操作的对象是不变的,因此,使用值对象也更容易进行函数式编程。