值对象 与 引用对象

原文地址: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接管了,它会释放内存、整理内存碎片(就可能会移动分配在堆上的对象),所以就禁止开发人员通过这个指针获取内存地址、计算偏移、释放内存等,以防出错,只能用它来“引用”托管堆上的对象。

参考


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值