值对象的隐藏值
介绍
价值对象的特征:
概括
链接:
介绍
在就 Vaughn Vernon 的《实现领域驱动设计》进行讲座之后,我决定写一篇与领域驱动设计相关的文章,重点关注被低估的价值对象。根据我的经验,开发人员在项目中滥用实体。这种行为通常会导致出现大量贫血的领域模型,这些模型缺乏或只有极少的业务逻辑。我认为,其中一些贫血的领域模型可以快速转换为价值对象。
注:所有示例均使用 C# 编写,并采用最新的 C# 语法。我希望使用其他语言的开发人员也能轻松理解。
例子:
让我们从一个简单的例子开始,例如下面代码片段中的 Car 实体。
public class Car
{
/* Properties */
public string Color { get; set; }
public bool Metallic { get; set; }
public string FuelType { get; set; }
public int EngineCapacity { get; set; }
{ ... }
/* Methods */
public void Drive(int distance) { ... }
{ ... }
}
我们将重点关注与汽车颜色相关的属性,即颜色和金属度属性。为简化起见,颜色属性用字符串类型表示。
价值对象的特征:
1) 测量、量化或描述某一领域的主题
在我们的示例中,Color`v` 和 `v`Metallic是 `Value` 对象的最佳候选对象。两者都描述了车辆的外观。因此,提取新类:
public class Color
{
public string Hue { get; set; }
public bool Metallic { get; set; }
}
每当Hue颜色Metallic发生变化时,我们都会收到一种新的颜色,所以让我们进入下一个要点。
2)是不可改变的
不可变性意味着对象在初始化后其状态不能更改。在 Java 或 C# 中,可以通过将所有参数传递给值对象的构造函数来实现这一点。对象的状态将根据参数值进行设置。当然,还可以根据这些值计算一些额外的属性。
注意:对象本身不能通过公共方法或私有方法调用属性设置器。这是被禁止的。
让我们根据前面的思考来完善我们的例子。
public class Color
{
public Color(string hue, bool metallic)
{
Hue = hue;
Metallic = metallic;
if (hue == "white" && !metallic)
{
BasicPallete = true;
}
}
public string Hue { get; }
public bool Metallic { get; }
public bool BasicPallete { get; }
}
在上面的例子中,我们计算了该BasicPallete属性。例如,它可能会影响汽车的价格计算。
3) 将关联的属性值组合成一个整体单元
这意味着所有属性都是有界的,每个属性都提供了描述对象整体价值的关键信息。单个属性的值并不能提供关于对象的完整信息。例如,知道某种颜色是否具有金属光泽并不能告诉我们它的色调。同样的情况也发生在我们知道色调,但却不知道它是否具有金属光泽的时候。
价值就是一个经常被提及的完美例子。我们以100美元为例。货币和金额是紧密相连的。假设有人将我们对象中的货币从美元换成了墨西哥比索(MXN)。这种变化会产生显著的影响。100墨西哥比索大约只值5.25美元。没有人愿意成为这种微小变化的受害者。
4)当测量结果发生变化时,可以进行替换。
在我们的例子中,汽车是一个实体,颜色是一个值对象。假设有一天我们决定改变汽车的颜色。假设我们买了一辆绿色的非金属漆汽车,但几年后,我们想换成绿色金属漆。我们不能直接在原有漆面上添加金属光泽,而是需要重新喷涂新的绿色金属漆。
Color color = new Color("green", false);
//color.Metallic = true; -> This is not allowed
color = new Color("green", true);
5) 可以与其他值对象进行比较
在 C# 等语言中,默认情况下,当我们尝试比较两个对象时,比较范围仅限于它们在内存中的位置(称为引用相等性)。两个对象可以拥有相同的属性值,但它们并不相等。对于值对象而言,这种假设是错误的。在这种情况下,我们需要比较给定类型及其所有属性值是否相等(称为值相等性)。
根据我们的例子,如果两种颜色色调相同,无论它们是否具有金属光泽,它们都是相同的颜色。两辆绿色金属漆的汽车颜色相同。
因此,让我们提升课堂教学水平,贯彻价值平等原则:
public class Color
{
public Color(string hue, bool metallic)
{
Hue = hue;
Metallic = metallic;
if (hue == "white" && !metallic)
{
BasicPallete = true;
}
}
public string Hue { get; }
public bool Metallic { get; }
public bool BasicPallete { get; }
public override bool Equals(object obj)
{
return this.Equals(obj as Color);
}
public bool Equals(Color otherColor)
{
if (Object.ReferenceEquals(otherColor, null)) return false;
if (Object.ReferenceEquals(this, otherColor)) return true;
if (this.GetType() != otherColor.GetType()) return false;
return (Hue == otherColor.Hue) && (Metallic == otherColor.Metallic);
}
public static bool operator ==(Color leftColor, Color rightColor)
{
if (Object.ReferenceEquals(leftColor, null))
{
// null == null = true
return (Object.ReferenceEquals(rightColor, null));
}
return leftColor.Equals(rightColor);
}
public static bool operator !=(Color leftColor, Color rightColor)
{
return !(leftColor == rightColor);
}
public override int GetHashCode()
{
return (Hue.GetHashCode() * 0x100000) + (Metallic.GetHashCode() * 0x1000) + BasicPallete.GetHashCode();
}
}
6)无副作用
这是基本规则。如果没有这条规则,Value 对象就可以被视为属性的简单容器。要理解它,我们应该首先理解无副作用函数。无副作用函数会产生结果,但不会改变其自身状态。这类函数在函数式编程范式中至关重要。
值对象公开的所有方法都应该是无副作用的函数,这样就不会违反值对象的不可变性。
我们以汽车重新喷漆为例。我们可以通过在类中添加这样的函数来实现重新喷漆。
public class Color
{
public Color(string hue, bool metallic)
{
Hue = hue;
Metallic = metallic;
if (hue == "white" && !metallic)
{
BasicPallete = true;
}
}
public string Hue { get; }
public bool Metallic { get; }
public bool BasicPallete { get; }
public Color Repaint(string hue, bool metallic)
{
return new Color(hue, metallic);
}
// Value Equality part
{...}
}
正如我们所见,这与汽车重新喷漆的情况类似。但方法和值对象之间的联系更紧密,且没有副作用。我们没有修改颜色对象的状态,而是创建了一个具有所需值的新对象。
Color color = new Color("green", false);
color = color.Repaint("green", true);
概括
当然,我并没有涵盖所有与值对象相关的主题,更遑论领域驱动设计(DDD),但我希望这篇文章能启发你在项目中更频繁地使用它。
PS
请不要走极端。
链接:
- Color.cs 类
- Color.cs 只读结构体
- .NET 指南中的值对象实现
- 来自 MartinFowler.com 的埃文斯分类
- 来自 MartinFowler.com 的值对象,其中包含用 Java 编写的示例。
