65.9K
CodeProject 正在变化。 阅读更多。
Home

nBaclava

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.71/5 (14投票s)

2012 年 10 月 1 日

CPOL

10分钟阅读

viewsIcon

52811

downloadIcon

439

值对象的基类。

目录 

引言

虽然 Baklava 对每个人来说都很美味,但它的技术伴侣 nBaclava 可能会让您作为开发者感到满意。 nBaclava 代表“BASE CLASSES FOR VALUE OBJECTS”(值对象的基类),为您提供 - 嗯 - 值对象的基类。如果您不熟悉值对象,我建议您阅读 CodePrpject 上 Richard A. Dalton 的这篇文章,但我也会给您一个简要介绍。

在领域驱动设计(DDD)中,值对象被描述为“一个描述某些特征或属性但没有身份概念的对象”。例如,.net 框架的 Point 结构就是一个值对象。它由其 x 和 y 坐标定义。

您可能会很自然地将一个点定义为其自己的类型,因为 x 和 y 坐标似乎非常不可分割。因此,将它们组合在一个 classstruct 中是很常见的 - 这就是面向对象编程的作用 Wink。但是原始值呢?您是否考虑过为 stringdecimal 定义一个 class?例如,电子邮件地址通常由一个 string 定义,金额由一个 decimal 定义。这似乎已经足够好了,并且一个如下所示的 class 在没有添加任何特殊功能的情况下看起来可能过于庞大。

public class EMailAddress {
    public string Value { get; private set; }
 
    public EMailAddress(string value) {
        Value = value;
    }
}

但请继续阅读...

值对象的优点

  • 根据定义,值对象应该是不可变的。这意味着一旦设置,其值就无法更改。这是函数式编程中的一个非常重要的事实,如 Marc Clifton 在这里所述,但函数式编程的优点或多或少可以转移到 OOP。
  • 定义您自己的 class - 即使是像上面所示的原始值(如 string) - 也让您有可能验证所使用的值。例如,如果您的 string 不能表示有效的电子邮件地址,您可以抛出异常。
  • public class EMailAddress {
        public string Value { get; private set; }
     
        public EMailAddress(string value) {
            if(value == null) {
                throw new ArgumentNullException("value");
            }
            // your tricky validation logic comes here...
            if(!value.Contains("@")) {
                throw new ArgumentException("Not a valid email address.");
            }
            Value = value;
        }
    }

    结合不可变性的概念,您现在可以确保始终拥有一个有效的电子邮件地址。并且它是验证逻辑代码的理想中心位置,否则这些代码会分散在一些实用工具类中。

  • 拥有自己的类型,您就更容易接受新功能。想象一下,您需要在程序中获取电子邮件地址的顶级域。如果 电子邮件地址 只是一个 string,您可能会在每次需要时复制粘贴所需的 string 操作。显然,您最终会再次得到一个实用工具类。但请查看值对象的可能性。
  • public class EMailAddress {
        public string Value { get; private set; }
     
        public TopLevelDomain TopLevelDomain {
            get {
                int topLevelDomainStartIndex = Value.LastIndexOf('.') + 1;
                string topLevelDomain = Value.Substring(topLevelDomainStartIndex);
                return new TopLevelDomain(topLevelDomain);
            }
        }
     
        public EMailAddress(string value) {
            // your tricky validation logic comes here...
            Value = value;
        }
    }
     
    public class TopLevelDomain {
        public string Value { get; private set; }
     
        public TopLevelDomain(string value) {
            Value = value;
        }
    }
  • 您可以定义合理的 null 值。例如 String.Empty,这样您就不必在整个应用程序中检查 null
  • 并且免费获得一个更结构化、更具可读性和更具表现力的软件设计。

我希望您可以看到,如果您仔细查看并切换到值对象,那么在使用原始值方面有更多的潜力。定义“金额”而不是直接使用 decimal,并且您可以自动将任何值四舍五入到小数点后两位。定义“年龄”而不是使用 int,并在需要时提供计算出的“YearOfBirth”属性...

值对象的缺点

除了您必须做一些工作来定义自己的类型 - 即使只是为了一个原始值 - 您可能会发现将值包装到类中然后再转换回原始值以在第三方 API(包括 .net 框架)中使用很麻烦,因为这些 API 不了解您的类型,只提供原始值作为方法参数或属性。

nBaclava

这就是 nBaclava 的作用。它为您提供通用的值对象(如上面提到的 Point)和特别是原始值(如 stringdecimal)的基类。此外,还有一些 Visual Studio 类模板可以使它的使用更加简单。继续阅读以了解 nBaclava 如何在值对象的每个概念上支持您。

ValueObject 与 PrimitiveValueObject 对比

nBaclava 提供两种主要类型:ValueObjectPrimitiveValueObjectValueObject 是您复杂类型(具有多个属性,例如 Point)的基类,而 PrimitiveValueObject 封装单个简单类型(如 stringdecimal 等)。对于后者,nBaclava 提供专用子类,如 StringValueObjectDecimalValueObjectPrimitiveValueObject 有一个名为 Value 的属性,该属性保存实际值(例如 string)。这与可空类型(如 int?DateTime?)上的内容相同。因此,您的电子邮件地址类的简单起始点看起来像这样:

public class EMailAddress : StringValueObject<EMailAddress> {
    public EMailAddress(string value)
        : base(value, FreezingBehaviour.FreezeOnAssignment, ValidityBehaviour.EnforceValidity) {
    }
}

PrimitiveValueObject 为您提供了对封装类型的许多实例方法的支持。例如,您可以直接调用 EMailAddress 值对象的 StartsWith 方法。

EMailAddress eMailAddress = new EMailAddress("someone@somewhere.com");
if (eMailAddress.StartsWith("someone")) {
    // do something
}

因此,无需为此类操作使用内部值。并且值对象的使用可读性更自然。

不可变性

如上所述,值类型应该是不可变的。这通常是通过在构造函数中获取值,然后仅为属性定义 get 而不定义 set 来完成的。但有些情况下这并不适用。例如,存在依赖于无参数默认构造函数的 ORM 映射器。对于更复杂的对象,在特殊构建过程中设置每个属性而不是使用带有许多参数的构造函数可能是一件好事。

因此,我决定实现冻结模式。这意味着实例在调用 Freeze() 方法之前是可变的。所以您可以决定何时冻结您的对象。

因为 PrimitiveValueObject 只有一个值,所以您可以通过将具体值和 FreezingBehaviour.FreezeOnAssignment(查看上面的代码片段)传递给基构造函数来缩短冻结过程。

这样,实例将在其第一次赋值时自动冻结。

您会发现 PrimitiveValueObject 默认情况下不提供其 Value 属性的 setter。不可变性是设计首选。如果您需要为您自己的类型设置 Value setter,您将需要像这样重写属性:

public new string Value {
    get { return base.Value; }
    set { SetValueTo(value);}
}

如果您相反实现自己的复杂 ValueObject,您将需要以这种方式实现您的属性:

private string mStringValue = String.Empty;
 
public string StringValue {
    get { return mStringValue; }
    set { Set(() => mStringValue = value); }
}

所有其他冻结工作都由基类完成!

为了满足冻结模式,有一些有用的属性:

CanFreeze(您可以为自己的类型重写它)、IsFrozenIsMutable

以及两个事件方法:

OnFreeze()OnFrozen(),如果您有需要,您可以处理它们。

一旦对象被冻结,就无法回退!

有效性

值对象应该默认有效,或者至少它们应该给您检查其有效性的可能性。所以您可以检查其 IsValid 属性。或者您可以调用 Validate() 方法,如果相应实例处于无效状态,该方法会抛出异常。为了仅获取异常,您可以调用 TryValidate()

您必须重写 TryValidate() 以定义您自己的验证逻辑。默认情况下,所有值对象始终是有效的。

要强制执行有效性,您必须将 ValidityBehaviour.EnforceValidity(如上所示)传递给基构造函数。这样,当属性值更改时,Validate() 会被自动调用。

对于 PrimitiveValueObject,您可以重写 OnValueChanging() 以在设置值之前对其进行调整(例如,当 null 时返回 String.Empyt)。这样,如果合适,您可以避免无效状态。

对于 ValueObject,您应该在不可变性下实现您的属性。

相等性

如开头所述,值对象仅由其属性描述。如果两个值对象的所有属性都相等,则它们是相等的。与默认类相比,默认类仅在它们的引用指向同一内存位置时才相等!以 Point 为例,您会同意所有 x=8 且 y=5 的点都是同一个点 - 无论它们的内存指针如何。nBaclava 的基类重写了 Equals() 以满足此策略。所以您不必做任何特别的事情 - 一切都完成了!对于 PrimitiveValueObject 来说这很容易,因为它只有一个值可以比较。对于 ValueObject,您的类的所有字段都通过反射收集,然后检查它们的值。这几乎与 .net 框架在 ValueType 类中执行的操作相同。

Null

值对象让您有机会通过在设置值之前更改给定值来定义 null 的合适替代项(例如,为 PrimitiveValueObject 使用 OnValueChanging())。因此,无需在您的应用程序的其余部分检查 null

如果您需要将 null 存储为有效值,您将很高兴地了解到,封装了 struct(例如 decimalintDateTime 等)的 PrimitiveValueObject 遵循可空模式,因此您也可以使用 decimal?int?

PrimitiveValueObject 提供 IsNull(或 HasValue)作为属性。

隐式转换

PrimitiveValueObject 提供隐式转换为其封装类型。这样,您就可以在需要基类型的所有场景中使用您的值对象 - 例如,作为方法参数或用于变量赋值。

EMailAddress emailAddress = new EMailAddress("someone@somewhere.com");
string email = emailAddress;

所以,在这种方式下无需使用 Value 属性。

string email = emailAddress.Value;

如果您使用了提供的 Visual Studio 类模板,您也将获得反向的隐式转换。

EMailAddress emailAddress = "someone@somewhere.com";

更多转换

PrimitiveValueObject 实现 IConvertible

运算符

PrimitiveValueObject 有一些其他运算符 - 不仅仅是隐式转换!因此,您可以使用算术运算符,如 +、-、*、/、<、<=、>、>=、!=、==,用于数字值类型,如 DecimalObjectValueIntValueObject

假设一个简单的类 Amount 如下:

public class Amount : DecimalValueObject<Amount> {
    public Amount(decimal value)
        : base(value, FreezingBehaviour.FreezeOnAssignment, ValidityBehaviour.EnforceValidity) {
    }
    public static implicit operator Amount(decimal value) {
        return new Amount(value);
    }
}

然后您可以像这样使用它:

Amount value1 = new Amount(12.34m);
Amount value2 = new Amount(2.10m);
Amount result = value1 + value2;

或与原始十进制值结合使用:

Amount result = value1 + 2.10m;

结果将是具体对象值类型的新实例!

为了使这能够工作,您需要提供一个带有一个参数的构造函数 - 即值(如上面的代码所示)。这就是为什么您的类需要它自己作为泛型参数的原因。这样,nBaclava 就可以按需构造您的类型。

Visual Studio 类模板

您可以下载 nBaclava 的模板包。它包含 PrimitiveValueObject 定义(如 DateTimeValueObjectStringValueObject)和通用 ValueObject 的类模板。因此,您可以在设置自己的值对象时非常快速。要使用这些模板,您需要将包解压到您的模板文件夹中。如果您不知道在哪里可以找到它或如何定义它,请在此处查看。安装这些模板后,当您在项目中调用“添加 - 类”时,您将看到一个新的类别 **nBaclava**。

通过这些模板创建的新类型可能看起来像这样:

/// <summary>
/// Encapsulates a <see cref="System.String">String</see> defined as 'EMailAddress'.
/// </summary>
public class EMailAddress : StringValueObject<EMailAddress> {
 
    /// <summary>
    /// Initializes a new instance of the <see
    /// cref="EMailAddress">EMailAddress</see> class with the specified value.
    /// </summary>
    /// <param name="value">Value.</param>
    public EMailAddress(string value)
        : base(value, FreezingBehaviour.FreezeOnAssignment, ValidityBehaviour.EnforceValidity) {
    }
 
    #region [ Operators ]
    /// <summary>
    /// Converts the specified value to an instance of the <see cref="EMailAddress">EMailAddress</see> class.
    /// </summary>
    /// <param name="value">The value which is converted to an
    /// instance of the <see cref="EMailAddress">EMailAddress</see> class.</param>
    /// <returns>A new instance of the <see cref="EMailAddress">EMailAddress</see> class.</returns>
    public static implicit operator EMailAddress(string value) {
        return new EMailAddress(value);
    }
    #endregion
}

NuGet

nBaclava 也作为 NuGet 包可用。只需在 NuGet 包管理器中搜索 nBaclava

结论

如果您看到了值对象的优点,但您或多或少太懒惰,无法在您的应用程序中一致地定义它们,那么我希望借助 nBaclava 您将变得更加一致。如果您不是懒惰类型,我希望您仍然会发现它很有用 Wink

我很想听听您对此的看法?如果您觉得它有用,我也会添加对 Int16Int64bool 等缺失类型的支持!

历史

  • 2012 年 10 月 1 日:布局已修复
  • 2012 年 9 月 30 日:第一个版本
© . All rights reserved.