值对象与引用对象

来源:互联网 发布:车载播放器软件 编辑:程序博客网 时间:2024/05/29 19:09

值对象和引用对象是面向对象设计经验的一种有效总结,并没有正式的定义。本文的写作目的就是讨论这些面向对象的基础概念,以在设计的过程中更有自信地使用对象。

对象和对象引用是数据类型层面上的划分,是物理上的概念。值对象和引用对象是应用层面的划分,是抽象上的概念。值对象和引用对象描述了对象的不同的共享模式。清楚两者的感念帮助我们更好的了解和设计系统。

对象与对象引用

在介绍何为值对象和引用对象之前,有必要先说一下对象和对象引用,它们是不同层次的概念。
这里的对象指的是对象实例,是实际存储对象内部状态的内存单元。对象引用指的是指向对象实例的指针,多个引用可以指向同一个对象实例,对象实例也可以没有被引用。

使用对象引用而非对象可以获得好处。对象引用占据极少的内存空间且分配在栈中,以极快的速度创建和释放。在参数传递中,传递引用比传递对象实例更高效;引用对象支持共享,当一个对象需要多次使用时,拷贝引用可以节省内存;保持对象的唯一实例可以做到只在一处修改就可以影响全部使用此对象的用户的效果。

引用对象使用不当会造成别名问题。

值类型似乎采用值传递,而对象采用引用传递,但是可以一致性的对待这一问题——所有变量拷贝都采用值传递的方式。这在C中显然是这样,指针是一种基本数据类型,传递指针以到达地址传递的效果。在java和C#中,可以把对象引用理解为一种新的数据类型,属于值类型的范畴,那么任何对象都是由这些基本类型组合而成,拷贝对象引用相当于一种地址传递。引用与指针的区别在于引用是自动解释的,任何出现引用变量的地方都会被自动解释为所引用的对象,没有进行地址运算的机会。

值对象

值对象描述了一种不可变的对象,这些对象完全由其所含的数据值在定义,并不介意副本的存在。这种对象仿佛和值类型一样,可以任意拷贝而不用担心别名的问题。如果你把同一个对象资源赋值给两个不同的变量,然后改变其中的一个变量,另一个变量仍然不受影响。
值对象的使用彻底避免了因为出现多个引用而产生的别名问题。
值对象往往是基本数据类型的包装。

如下演示了值对象的使用与使用动机:
    class Person    {        private Money _money;        public Person(double hasMoney)        {            _money = new Money(hasMoney);        }        public Money GetMoney()        {            return _money;        }    }    class Program    {        static void Main(string[] args)        {            Person aPerson = new Person(1000.0);            Console.WriteLine(aPerson.GetMoney().Dollars);        }    }
Money类封装了钱的表示方式和存储结构,使用封装类而不是基础数据类型的好处是你可以任意扩展Money使之支持各种货币的表示方法和兑换方法而不用修改客户代码。

Person类与Moeny类的关系是组合,Person组合了Moeny,两者具有相同的生命期。

如果Money对象不是值属性的,也就是可以改变其内部数据,那么上述代码是危险的。Person类可以拒绝外部对Money对象引用的赋值,却无法拒绝对Moeny对象自身的修改。客户可以在任何时候修改Moeny的内部状态而直接反应到Person对象的Moeny成员中,不需要经过Person的同意。这种设计是不安全的。

如果Moeny对象是值属性的,也就是其内部状态是不可变的,那么上述代码是安全的。客户即使获得了moeny对象的多个引用拷贝,都无法修改money,因为其不提供设值操作。

值对象具有双重优点,既支持共享同一个实例引用以节省内存,又可以保证数据安全而不会产生别名问题。

值对象设计

值对象是一个使用概念,并没有强制限制不可以提供设置函数,可以只是一种约定,约定这个对象不被共享或者客户不要改变它就可以了。不过我们应该努力通过代码设计来解除复杂约定,这也体现了软件设计的根本目的——让客户更轻松。

可以通过隐藏数据成员并且不提供设置函数来强制将一个对象变为值对象,进而上升到设计模式的层次。
    class Money    {        private double _dollars;        public Money(double dollars)        {            this._dollars = dollars;        }        /// <summary>        /// 获取美元        /// </summary>        public double Dollars        {            get { return this._dollars; }        }    }

这样当实际需要改变Moeny对象时,就得重新使用构造器创建一个新对象,这个新对象与旧对象无关。不能提供拷贝构造函数,因为这也会改变内部状态。
        public void SetMoney(Money money)        {            _money = new Money(money.Dollars);        }

要想使两个具有相同值的不同对象在相等性判断中判定为相等,可以重载一些运算符,大小比较同理。设计目的是使值对象更具有值类型的特征。
        public override bool Equals(object obj)        {            if (obj is Money)            {                Money o = (Money)obj;                return this.Dollars.Equals(o.Dollars);            }            return false;        }        /// <summary>        /// 获取哈希码,唯一标示对象状态        /// </summary>        public override int GetHashCode()        {            return (int)this.Dollars;        }        public static bool operator ==(Money m1, Money m2)        {            //如果具有相同引用或同时为null,返回true            if (System.Object.ReferenceEquals(m1, m2))            {                return true;            }            //如果其中一个为null, 返回false            if (m1 == null || m2 == null)            {                return false;            }            return m1.Equals(m2);        }        public static bool operator !=(Money m1, Money m2)        {            return !m1.Equals(m2);        }

值对象可以扩展以增加运算操作,通过返回一个新对象的方式来达到运算的目的。
        /// <summary>        /// 获取美元        /// </summary>        public double Dollars        {            get { return this._dollars; }        }        public Money AppendMoeny(Money money)        {            return new Money(this.Dollars + money.Dollars);        }        public Money SubMoney(Money money)        {            return new Money(this.Dollars - money.Dollars);        }
这似乎是一个延迟创建技术的应用之一,即只在需要新对象的时候才会创建新对象。
string类的对象就是一个经典的值对象。string对象具有值类型的特点,却可以高效的拷贝,其只在使用时产生一个新的string对象。


在C#,java中,不能直接创建一个对象变量,只能通过多余创建一个引用来指向对象实例。在C++中,可以直接创建对象变量而不需要创建引用或指针,所以C++中的对象可以值的方式传递,类似于C#中的结构体。随然C++中的对象变量是值类型的,可以并不一定符合值对象的标准,因为仍然可以通过指针和引用修改其内部状态。

不变模式

在参考相关资料的过程中发现原来存在一个不变模式,所以插入本节以阐述我对其的理解,对不变模式的学习请查阅相关资料。
前文有提到,如果在设计之初就考虑到值对象的使用并在类设计上支持值对象,那么就上升到设计模式的层次,这个模式被称为不变模式,是一种对象行为型模式。
不变模式强调一个类所产生的对象一定是不变的,这种不变性可以理解为任何时刻调用查询方法都会返回相同的结果,这个对象一定是值对象。设计目标是让对象引用具有值类型的特点,让使用和维护更简单,且是线程安全的。

不变模式产生一个不变类,不变类是一个塑造出值对象(或者称之为不变对象)的模板。

结构体陷阱

在C#中,可以使用结构体来生成一个值对象,结构体和值类型一样,在栈中创建并且不可以被引用。结构体保证实例不会出现别名问题,因为它不可以通过指针来访问,因为C#中没有指针,引用变量由类来定义。
你无法声明一个结构体引用,试图拷贝或传递结构体变量只会产生新的副本。

修改上述案例,把class改为struct,并增加一个设置函数:
    struct Money    {        private double _dollars;        public double Dollars        {            get { return _dollars; }            set { _dollars = value; }        }        public Money(double dollars)        {            this._dollars = dollars;        }    }
由于struct本身就是值类型的,所以任意添加设置函数之后money仍然是一个值对象。

但这样做会出现更为严重的隐患,我之前也遇到很多令人头疼的问题。
原因是客户总是把一个struct变量当做一个对象来使用,因为它们都拥有方法成员和数据成员。当拷贝变量时,客户会以为拷贝的是引用而非对象实例,客户企图通过设置函数来修改传递过来的结构体变量。使用结构体类型的目的之一是为了解决别名问题,不让一个对象状态的修改影响到另一个,但任何事情都有两面性,如果你偏要使一个对象状态的效果影响到另一个,别名问题又产生了。
Person对象通过了一个方法可以访问Moeny对象,但是由于其是值类型的,客户只是对其临时副本的访问而已,调用money的设置函数也只是修改副本的状态而已,并不会影响原始的money对象。如果你以为正确的使用了对象引用来修改对象状态,那么你就中了struct的陷阱。

值对象取代struct变量。在C#中尽量少使用struct,可以使用值对象设计模式来模仿出与struct一样的效果,并且可以更好的共享。struct变量虽然在存储和调用上更快速,但是值拷贝的高消耗会掩盖这种优势。

引用对象

引用对象描述了一种可以被共享(复用)的对象,这些对象的类往往在设计之初就考虑到共享的可能性或者打算用于共享。每个对象对应一个抽象化的实体,这个实体会被多次引用,并且希望这些引用对应一个相同的实例,通过任何一个引用都可以使用或改变这个对象的状态。可以使用相等操作符(==)检查两个对象是否相等。

被引用的实体对象在面向对象系统中如资源一般的存在,我们所提及的对象资源大多指的是可以被复用的对象,引用对象就是这些可以被重复使用的资源。”引用对象“不是一个动宾短语,而是一个从经验中总结出的概念。

如下代码描述了引用对象的使用与使用意图
    class Monster    {        private Bitmap _picture;        private string _name;        public Monster(string name, Bitmap pic)        {            _name = name;            _picture = pic;        }        /// <summary>        /// 获取图像        /// </summary>        public Bitmap Picture        {            get { return this._picture; }        }        public string Name        {            get { return this._name; }        }    }
    class Program    {        static void Main(string[] args)        {            Bitmap aBitmap = new Bitmap("face");            Monster a = new Monster("史莱姆1", aBitmap);            Monster b = new Monster("史莱姆2", aBitmap);        }    }
这里的bitmap就是一个引用对象,引用一个位图资源。在类关系图中Monster与Bitmap是一种单向关联(依赖)关系,并不是组合,因为bitmap对象是在monster生命期外创建的,也会在生命期外被释放。
Bitmap是非常消耗内存的资源,并且大多数情况下不会被独占,为每个Monster对象都存储一份拷贝是高消耗且没有意义的。

因为任何对象都可以被引用,所以这里讨论的引用对象指的是被设计用于共享或已经被共享的对象,并且大多数情况下允许通过引用修改对象状态。引用对象本身不需要增加设计,但是需要根据特定的使用意图和上下文环境来恰当管理这些引用对象,换言之,引用对象比值对象更难以维护。


无状态对象

无状态对象指的是不存在状态的对象,这些对象在系统运行的任何时刻调用同一方法都会产生相同的结果。所以其来类设计上不得不消除数据成员。
无状态对象一定是值对象,而值对象不一定是无状态对象。无状态对象不允许有数据成员,因为数据成员的存在目的是为了记录对象运行的时序状态,即使被隐藏,也会影响到不同时刻的方法调用。值对象可以拥有状态,你可以访问这些状态。

无状态对象可以作为值对象的一种实现方法,只要不存储数据就不用担心变化的问题,可以向值类型一样任意拷贝和复用,但这样做是没有意义的。值对象的设计意图在于共享且仅共享对象引用,且一个对象的改变不会影响另一个。我们常常把字符串、日期、货币、向量、坐标等包装为一个值对象,希望只在发生变化的时候才创建一个新的对象,可以通过操作弥补普通数据类型的缺陷,却不希望这些对象与值类型变量有所区别。没有状态就不能发挥值对象的优势,不能体现值对象的设计意图,所以我们并不把无状态对象与值对象关联起来。
无状态对象同时也是一个引用对象,因为其可以被大量的共享并且不会出现问题。当一个对象资源是一个服务时,可以将引用对象设计成无状态的对象。我们往往采用静态类或单例类来体现这种设计,当然也可以是一个临时创建的普通对象,如果不追求共享的话。


值对象与引用对象

上述文字对值对象和引用对象单独做出了诠释,现在对比一下两者以产生更目前的认识。
从划分标准上,两者是对面向对象设计中对象的一种有用归类。在面向对象系统中,共享的概念非常重要,区分一个对象是否支持共享与是否会因为共享而出现问题也同样重要。
从设计意图上,两者支持了不同程度的共享。共享是指重复使用同一个对象而不立即丢弃,或者同时使用一个对象而各自保留引用(当然不是指代码共享,而是运行时的共享)。我理解的共享有两次层面,一是共享数据,即任何一个引用有权利来修改对象,因为这个对象从需求上是共享的。另一种是共享访问,即只允许通过引用发送请求而不能修改数据,因为这个对象从需求上不允许完全共享。值对象的设计意图在于共享且仅共享对象引用,并在数据上抵抗共享,引用对象才是完全的共享。
从实现上,值对象需要类设计上支持,引用对象在对象交互与程序运行中体现出来。


重构

可以在设计上做一些有用的重构工作。将引用对象改为值对象或将值对象改为引用对象可以改善系统设计并使之更易于理解,将重要的值对象和引用对象加入共享池可以扩大这种设计的价值。

关于这方面的认识可以参考《重构 改善既有代码的设计》。













0 0