实体与值对象的区别
在面向对象的软件设计中,万物皆为对象。甚至在Java中,为了保证万物皆为对象,还为基本类型设置了包装类。但即使这样,这些对象也需要我们进一步地进行区分。
假设存在下面的对象
public class User {
private Integer id;
private String name;
private String address;
private String email;
}
显然User是我们定义的一个对象,而内部则是该对象的属性。
我们继续分析这几个属性:
name属性:按照国人的习惯,最好是姓氏、名字单独存放,以便于在必要时展现“李先生”、“刘女士”等这样的称呼。使用一个简单的String存储显然无法实现。我们可以将name属性拆分为firstName和lastName两个属性,但这样一来,姓氏与名字的拼接工作必须由User对象负责。但是,总感觉不合适,因为所有人的姓氏和名字的拼接规则都一样,似乎不应该有独立的User完成拼接。
adress属性:地址是一个长长的字符串,包含省、市、县等信息,更关键的,还有非必填的邮编等信息。如果我们把他们都放在一个String里面,显然过于混乱,也面临不好拆分的问题。于是,我们可以考虑将其作为一个地址对象存储,让地址对象包含各个子信息。但这时又让人感觉怪怪的,因为这个对象似乎难以复用。即使用户A和用户B填写了相同的地址也不好复用这一地址,因为不能让一方的修改影响另一方。
email属性:电子邮件地址有着固定的规则,在存入前需要校验。如果使用String存放电子邮件地址,则校验规则要放在别处,这破坏了内聚性。最好是让email属性自身完成。
再进一步分析,我们发现id属性也是如此。id不一定是数字,可能是一个字符串。而且,id可能包含了一套生成和校验规则,例如我们的身份证号就包含了所属人的籍贯、生日、性别等信息。
分析完以上几个属性后,我们发现这些属性不是基本类型,但是又和User这种对象不同。例如我们可以分别定义Name、Address、Email类,他们介于User这类对象和基本类型之间。
这些对象具有以下的特点:
- 没有编号,不能进行相等与否的比较。除非对象内存地址相同,否则他们就是不等的。
- 不能更新,很像是一个基本值。如果我们要修改它,则直接生成一个新的它即可,而不是更新它。这意味着我们可以放心地多处引用同一个它,而不用担心一处修改了它影响别处。这点和String的不可更新特性一样。
- 他们具有业务意义,仅从名字就能看出他们的作用。这一点是十分友好的。
- 他们可以集成相关功能。因为他们本身就是对象,内部可以实现相关的验证逻辑等各类逻辑。因此是一个包含属性和行为的整体。
以上这种对象,我们称之为值对象。值对象用来表示属性的不变值和属性的行为。
在面向对象的编程中引入值对象能够提升代码的可读性,提升内聚性。
而像User这样的对象则称为实体。实体和值对象是不同的,实体存在唯一性标志,而实体是否相等的判断依据就是唯一性标志。
例如,两个User对象,他们的地址可能并不相同,但是只要两者的id一样,则这样两个对象就是相等的。假设User01存放在内存中,将其序列化再反序列化后得到User02,则User01和User02的地址并不相同,但因为两者id一样,实际为一个对象。
另外再说一点,实体和值对象的界限不是绝对的。
例如在一个外卖系统中,地址是一个值对象,从属于人;在社区管理系统中,地址则是一个实体,与人存在多对多的关系。具体的划分要根据业务场景来确定。