值对象模式是一种设计模式,它强调对象的相等性是基于其值而不是其身份。在软件开发中,经常需要处理各种数据,这些数据的比较和处理方式对于系统的逻辑和性能都有重要影响。值对象模式提供了一种方式,使得对象的比较基于它们的值而不是它们在内存中的地址。本文将详细讨论值对象模式的定义、实现以及与数据传输对象(DTO)的区别。
值对象模式定义为:“值对象是一个对象,其等价性是基于值而不是身份的。”这意味着,即使两个对象的值相同,它们也可能代表不同的实体。例如,在下面的代码中,创建了两个具有相同姓名“Shiv”的人对象,但一个居住在“印度”,另一个居住在“尼泊尔”。因此,即使人的名字和年龄相同,这两个对象也是不同的,它们具有不同的“身份”。
Person PersonfromIndia = new Person();
PersonfromIndia.Name = "Shiv";
PersonfromIndia.Age = 20;
Person PersonfromNepal = new Person();
PersonfromNepal.Name = "Shiv";
PersonfromNepal.Age = 20;
在C#中,如果尝试比较上述对象,它将返回false,这完全符合预期。可以使用“Equals”方法或使用“==”运算符。
值对象和数据传输对象(DTO)是两个不同的概念。DTO主要用于简化层与层之间的数据传输,而值对象则用于使对象基于值而不是引用进行比较。值对象通常是不可变的,而DTO则不一定。以下是值对象和DTO的比较:
在C#中实现值对象模式是一个两步过程。首先,需要确保值对象在逻辑上能够正确地工作。这意味着需要重写“Equals”方法和重载“==”运算符,以便比较基于值而不是引用。以下是一个示例代码:
class Money
{
public override bool Equals(object obj)
{
var item = obj as Money;
if ((item.Value == Value) && (item.CurrencyType == CurrencyType))
{
return true;
}
return false;
}
public static bool operator !=(Money money1, Money money2)
{
if ((money1.Value != money2.Value) && (money1.CurrencyType != money2.CurrencyType))
{
return true;
}
return false;
}
public static bool operator ==(Money money1, Money money2)
{
if ((money1.Value == money2.Value) && (money1.CurrencyType == money1.CurrencyType))
{
return true;
}
return false;
}
}
一旦上述方法被整合,等价性将基于值而不是引用。这与在引言中讨论的值对象模式的行为是一致的。
现在让假设在产品类中使用上述货币对象。可以看到创建了两个产品对象,一个用于鞋子,一个用于巧克力。但是,由于错误,为两个产品分配了相同的“Money”对象。换句话说,货币对象现在在两个产品之间共享。这可能会导致很多缺陷和异常行为。
值对象应该是不可变的,以避免混淆。如果对不可变的概念不熟悉,建议阅读这篇文章关于C#不可变对象。
class Money
{
private readonly double _Value;
public double Value
{
get { return _Value; }
}
private readonly string _CurrencyType;
public string CurrencyType
{
get { return _CurrencyType; }
}
private readonly string _Material;
public string Material
{
get { return _Material; }
}
public Money(double _value, string _currencytype, string _material)
{
_Value = _value;
_CurrencyType = _currencytype;
_Material = _material;
}
}
以下是如何将产品对象分配给不可变货币对象的代码。这将进一步避免对货币对象的更改,以及围绕同一对象的任何混淆。现在鞋子的货币值对象与巧克力的不同。
Product shoes = new Product();
shoes.ProductName = "WoodLand";
shoes.Cost = new Money(100, "INR", "Paper");
Product Chocolates = new Product();
Chocolates.ProductName = "Cadbury";
Chocolates.Cost = new Money(1, "USD", "Coin");
还有一个场景,期望值对象能够正常工作,即与Hashtable集合一起使用。假设将货币对象添加到Hashtable集合中,如下所示:
Money m1 = new Money(1, "INR", "Coin");
Hashtable coll = new Hashtable();
coll.Add(m1, m1);
现在应该能够通过创建具有相同值类型的其他货币对象来检索相同的对象,因为它们具有相同的值。换句话说,以下代码应该能够无问题地检索到“m1”货币对象。
Money m2 = new Money(1, "INR", "Paper");
Money m3 = (Money)coll[m2];
Hashtable使用“GetHashCode”的值从集合中获取对象。因此,为了确保值和货币类型的组合的哈希码计算相同,需要重写“GetHashCode”函数,如下所示。通过这样做,货币对象将能够正确地与Hashtable集合一起工作。
class Money
{
// Code removed for clarity
public override int GetHashCode()
{
return (_Value + _CurrencyType).GetHashCode();
}
}
感谢Ajay指出这个场景。
许多开发人员使用结构体来实现值对象设计模式,可能由于结构体的技术性质,这看起来也是合乎逻辑的。但是个人认为使用结构体实现时遇到了一些问题:
值对象的等价性是基于值而不是身份的。值对象应该是不可变的,以避免混淆。在C#中,为了确保值对象的正确行为,需要重写“Equals”方法和“==”运算符。值对象与DTO有很大的不同,它们的目的和特性都不同。