原文地址:http://www.w2bc.com/article/95941
值对象(value object)
什么是值对象
维基百科的定义
In computer science, a value object is a small object that represents a simple entity whose equality is not based on identity
在计算机科学中,值对象是一种小型对象,它表示一个简单的实体,该实体的相等性不取决于标识。
我们身边就有一堆“值对象”,比如你现在从钱包掏一张纸币,它就是典型的值对象,纸币存在的意义在于表示一个价值。我们区分纸币时并不是根据纸币上那个唯一的编号,而是根据它表示的面值,具有相同面值的纸币就是等价的,是可互换的。
这里纸币的编号就对应程序中对象的标识,纸币的面值就对应程序中对象表示的值。
其它值对象的例子:ip地址、rgb颜色、地理坐标。
我的解理就是:值对象是用来表示一个量、一个值、一个信息的对象,它的状态是静态的;我们使用值对象时,关心的是它所表示的信息,而不是这个对象本身
值对象与不可变性(immutability)
值对象通常都会被设计成不可变的(immutable),比如.NET基类库中的Int32、DateTime这些类型都是不可变的。
可能有人会对下面的C#代码提出疑问:
Int32 i = 1;
i = 2;
咦!这不是可变的吗?注意这里i是一个l-value(左值)
,是一个存放Int32类型值的变量,是线程栈上一个内存块,是一个容器,相当于是钱包;下一句中只是把该钱包中的纸币换了,并没有修改纸币的面值。像某日期.月份 = 12
, 某整数.符号 = 负
才叫修改了对象。
为什么说值对象需要是不可变的
主要有以下原因:
- 从理论上讲值对象存在的意义就在于表示一个值,如果状态修改了,那就是一个新的值,应该用新的对象表示。
-
简化编程、避免bug
如果值对象可变,当它被共享时,一处修改可能会影响另一处,修改操作就会产生“副作用(side effect)”,如java中的java.util.Date是引用类型并且是可变的,下面的代码演示了使用它时可能犯的错误:这里的问题在于我们关心的其实只是一个“日期值”而不是一个
java.util.Date
对象,所以在传递时应该是传递表示的日期值,而不是直接传递对象。注释中的FIXME标注是一种解决办法。 -
//表示一个定时任务 class Task{ //设置执行时间 public Date setStartDate(Date date){ this.startDate = date; } //获取执行时间 public Date getStartDate(){ return this.startDate; } //FIXME: 改成 return this.startDate.clone(); //延迟执行时间 void delay(int delayDays) { this.startDate.setDate(startDate.getDate() + delayDays); } private Date startDate;//执行时间 } task1.setStartDate(new Date("2016/01/01"); task2.setStartDate(task1.getTaskDate()); task2.delay(5); //这里本想修改task2的日期,但无意中影响到了task1
值对象必须要实现为不可变吗?
但是如果把一个类实现为不可变的话,意味着修改一个成员就要创建一个新的对象,如果成员非常多的话,代码写起来会比较繁琐。
有些语言支持值语义(value semantics),所谓“值语义”是指对象传递时是传递的它所表示的值(传值),而不是传递对象本身(传引用),这就意味着传递过程就是“复制”,比如C++默认就是值语义、C#中的struct等,因为是复制,所以值对象不会被共享,也就没有了上面提到的那个问题,这种情况下,值对象可变的话也是完全可以的,但是值对象仍可以被“按引用”传递,所以把值对象实现为不可变是最保险的手段。
值对象的实现
- C++
C++默认就是值语义,如果类状态较简单,则可以不做任何特殊处理。如果比较复杂,比如成员是指针或引用,则就要实现生命周期管理函数 - C#
- 如果类状态较简单,可直接用struct,因为struct正好支持值语义
- 如果状态较复杂则用class,并且从接口上把它设计成不可变,比如:public readonly的属性,提供一个能够初始化所有成员的构造方法 或 提供修改状态的方法,但实际不修改本对象的状态,而是构建并返回一个具有的新状态的新对象。
- java
同C#中的class
其它
struct与class
不管是C++和C#中的struct关键字,还是ruby中的Struct::new都是趋向用于定义简单的复合类型,所以struct适合用来定义没有复杂行为和状态的值对象。而class更趋向用于定义具有丰富逻辑、复杂状态的对象类型,struct、class和值对象、引用对象并不是一一对应的关系。
特殊的值对象 - 字符串
在我看来字符串是值对象,理由很简单:它的相等性取决于它的状态
虽然它本质是一个字符容器,但我们更多的是用它来进行比较,输出等等,而不是对里面的字符进行“增删改查”,所以从它的用途来看,它是一个静态的字符序列。
那么问题来了,为什么在C#和java中字符串都是引用类型呢?
现阶段我是这么认为的:
- “值类型”,“引用类型”这些只是语言/平台实现中的术语,“值对象”、“引用对象”是语言无关的,是对象设计思想,所以不是说被实现为“引用类型”它就不是值对象了
- 字符串本质上是字符容器,大小不定,不像Int32固定占4字节,且无法在编译时确定(当然字面量除外),如
String str = new String('a', 10)
,因此无法分配于栈上
虽然在C#中也完全可以把字符串实现为struct,在内部动态分配字符数组,但由于这样内部实现会比较复杂,而struct更倾向于用来定义简单的复合类型。
DTO(Data transfer object)
它和值对象的区别在于:值对象可以是一个domain model,可以拥有逻辑;而DTO被用于打包其它对象的状态,在layer间传递,是一种贫血模型,它没有逻辑。
引用对象(reference object)
什么是引用对象?
引用对象是相等性取决于它的标识的一种对象。
为什么要“相等性取决于标识”?因为在使用引用对象时,我们关心的是这个对象本身,在传递过程中,需要传递同一个对象,因此就把对象的“引用(即标识、通常是内存地址)”传递过去,这也就是“引用语义(reference semantics)”。
我们实际写程序中,使用对象的目的在于映射问题域中的事物,因此基本关心的是对象本身,所以使用引用对象会相对比较多。
实现
- 在C#和java中用class定义的类型叫“引用类型”,具有引用语义,创建的对象天生就是引用对象
- 在C/C++中通常通过动态分配内存和传递指针实现
指针 与 引用类型
- C/C++中的指针其实是一个unsigned int,表示一个内存地址。具体类型的指针类型如
int *
,类型名的用途在于解引用时知道数据的大小和数据表示的含义,我们可以用“强制类型转换”以不同的大小和类型解读一块内存,非常灵活,但也容易出错。 - C#和java中的引用实际上是一个受限的、托管的、安全的指针,因为内存管理被GC接管了,它会释放内存、整理内存碎片(就可能会移动分配在堆上的对象),所以就禁止开发人员通过这个指针获取内存地址、计算偏移、释放内存等,以防出错,只能用它来“引用”托管堆上的对象。