面向对象中的实体和值对象

标签: 架构设计

保留所有版权,请引用而不是转载本文(原文地址 https://yeecode.top/blog/26/ )。

在面向对象的软件设计中,万物皆为对象。甚至在Java中,为了保证万物皆为对象,还为基本类型设置了包装类。但即使这样,这些对象也需要我们进一步地进行区分。

假设存在下面的对象:

public class User {
    private Integer id;
    private String name;
    private String address;
    private String email;
}

显然User是我们定义的一个对象,而内部则是该对象的属性。

我们继续分析这几个属性:

name属性。按照国人的习惯,最好是姓氏、名字单独存放,以便于在必要时展现“李先生”、“刘女士”等这样的称呼。使用一个简单的String存储显然无法实现。我们可以将name属性拆分为firstName和lastName两个属性,但这样一来,姓氏与名字的拼接工作必须由User对象负责。但是,总感觉不合适,因为所有人的姓氏和名字的拼接规则都一样,似乎不应该有独立的User完成拼接。

address属性。地址是一个长长的字符串,包含省、市、县等信息,更关键的,还有非必填的邮编等信息。如果我们把他们都放在一个String里面,显然过于混乱,也面临不好拆分的问题。于是,我们可以考虑将其作为一个地址对象存储,让地址对象包含各个子信息。但这时又让人感觉怪怪的,因为这个对象似乎难以复用。即使用户A和用户B填写了相同的地址也不好复用这一地址,因为不能让一方的修改影响另一方。

email属性。电子邮件地址有着固定的规则,在存入前需要校验。如果使用String存放电子邮件地址,则校验规则要放在别处,这破坏了内聚性。最好是让email属性自身完成。

再进一步分析,我们发现id属性也是如此。id不一定是数字,可能是一个字符串。而且,id可能包含了一套生成和校验规则,例如我们的身份证号就包含了所属人的籍贯、生日、性别等信息。

分析完以上几个属性后,我们发现这些属性不是基本类型,但是又和User这种对象不同。例如我们可以分别定义Name、Address、Email类,他们介于User这类对象和基本类型之间。这些对象具有以下的特点:

以上这种对象,我们称之为值对象。值对象用来表示属性的不变值和属性的行为。

在面向对象的编程中引入值对象能够提升代码的可读性,提升内聚性。

而像User这样的对象则成为实体。实体和值对象是不同的,实体存在唯一性标志,而实体是否相等的判断依据就是唯一性标志。

例如,两个User对象,他们的地址可能并不相同,但是只要两者的id一样,则这样两个对象就是相等的。假设User01存放在内存中,将其序列化再反序列化后得到User02,则User01和User02的地址并不相同,但因为两者id一样,实际为一个对象。

另外再说一点,实体和值对象的界限不是绝对的。例如在一个外卖系统中,地址是一个值对象,从属于人;在社区管理系统中,地址则是一个实体,与人存在多对多的关系。具体的划分要根据业务场景来确定。

接下来,我们会在实体和值对象的基础上继续探讨《实体和值对象的聚合》。

本文首发于个人知乎:易哥(https://www.zhihu.com/people/yeecode),欢迎关注。

作者书籍推荐