维基百科的定义
In computer science, a value object is a small object that represents a simple entity whose equality is not based on identity在计算机科学中,值对象是一种小型对象,它表示一个简单的实体,该实体的相等性不取决于标识。
我们身边就有一堆“值对象”,比如你现在从钱包掏一张纸币,它就是典型的值对象,纸币存在的意义在于表示一个价值。我们区分纸币时并不是根据纸币上那个唯一的编号,而是根据它表示的面值,具有相同面值的纸币就是等价的,是可互换的。
这里纸币的编号就对应程序中对象的标识,纸币的面值就对应程序中对象表示的值。
其它值对象的例子:ip地址、rgb颜色、地理坐标。
我的解理就是:值对象是用来表示一个量、一个值、一个信息的对象,它的状态是静态的;我们使用值对象时, 关心的是它所表示的信息,而不是这个对象本身
值对象通常都会被设计成不可变的(immutable),比如.NET基类库中的Int32、DateTime这些类型都是不可变的。可能有人会对下面的C#代码提出疑问:
Int32 i = 1; i = 2;
咦!这不是可变的吗?注意这里i是一个 l-value(左值)
,是一个存放Int32类型值的变量,是线程栈上一个内存块,是一个容器,相当于是钱包;下一句中只是把该钱包中的纸币换了,并没有修改纸币的面值。像 某日期.月份 = 12
, 某整数.符号 = 负
才叫修改了对象。
主要有以下原因:
简化编程、避免bug如果值对象可变,当它被共享时,一处修改可能会影响另一处,修改操作就会产生“副作用(side effect)”,如java中的java.util.Date是引用类型并且是可变的,下面的代码演示了使用它时可能犯的错误:
//表示一个定时任务 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
这里的问题在于我们 关心的其实只是一个“日期值”而不是一个 java.util.Date
对象 ,所以在传递时应该是传递表示的日期值,而不是直接传递对象。注释中的FIXME标注是一种解决办法。
但是如果把一个类实现为不可变的话,意味着修改一个成员就要创建一个新的对象,如果成员非常多的话,代码写起来会比较繁琐。
有些语言支持 值语义(value semantics) ,所谓“值语义”是指对象传递时是 传递的它所表示的值 (传值),而不是 传递对象本身 (传引用),这就意味着传递过程就是“复制”,比如C++默认就是值语义、C#中的struct等,因为是复制,所以值对象不会被共享,也就没有了上面提到的那个问题,这种情况下,值对象可变的话也是完全可以的,但是值对象仍可以被“按引用”传递,所以把值对象实现为不可变是最保险的手段。
不管是C++和C#中的struct关键字,还是ruby中的Struct::new都是趋向用于定义 简单的复合类型 ,所以struct适合用来定义没有复杂行为和状态的值对象。而class更趋向用于定义具有丰富逻辑、复杂状态的对象类型,struct、class和值对象、引用对象并不是一一对应的关系。
在我看来字符串是值对象,理由很简单:它的相等性取决于它的状态
虽然它本质是一个字符容器,但我们更多的是用它来进行比较,输出等等,而不是对里面的字符进行“增删改查”,所以从它的用途来看,它是一个 静态的字符序列 。
那么问题来了,为什么在C#和java中字符串都是引用类型呢?现阶段我是这么认为的:
String str = new String('a', 10)
,因此无法分配于栈上 它和值对象的区别在于:值对象可以是一个domain model,可以拥有逻辑;而DTO被用于打包其它对象的状态,在layer间传递,是一种贫血模型,它没有逻辑。
什么是引用对象?
引用对象是相等性取决于它的标识的一种对象。
为什么要“相等性取决于标识”?因为在使用引用对象时,我们 关心的是这个对象本身 ,在传递过程中,需要传递同一个对象,因此就把对象的“引用(即标识、通常是内存地址)”传递过去,这也就是“ 引用语义(reference semantics) ”。
我们实际写程序中,使用对象的目的在于映射问题域中的事物,因此基本关心的是对象本身,所以使用引用对象会相对比较多。
int *
,类型名的用途在于解引用时知道数据的大小和数据表示的含义,我们可以用“强制类型转换”以不同的大小和类型解读一块内存,非常灵活,但也容易出错。