如何创建必填字段值对象

How to create a Required Field Value Object

我正在尝试创建一个 必填字段 值对象 class,它将在我的域模型中的实体之间重复使用。我仍在学习 C# 语法(一直在 VB.net 编码)。而且我是 DDD 的新手(但至少读过几本书)。

我的目标是设计一个名为 RequiredField<T> 的值对象,它可以接受任何对象(例如值类型、引用类型或可空值类型(int、string、int?))在允许它成为实体状态的一部分之前进行验证。只要我的实体有必填字段(例如 ID、主键、名称或任何其他被认为对拥有有效实体必要的状态),我就会使用此值对象。因此,只要我有一个有效实体所需的简单数据,实体就会将属性定义为 RequiredField<T>

所以我一直在摆弄这个 class,而且我越来越接近了,但似乎每次我认为我快要得到它时,我 运行到另一个绊脚石。这是我希望使用的样子,来自我的实体 class:

public class PersonEntity
{
    public RequiredField<long> ID { get; private set; }
    public RequiredField<string> Name { get; private set; }
    public RequiredField<DateTime> DOB { get; private set; }
    // define other non-required properties ...

    public PersonEntity(PersonDTO dto)
    {
        ID = new RequiredField<long>(dto.ID);
        Name = new RequiredField<string>(dto.Name);
        DOB = new RequiredField<DateTime>(dto.DOB);
        // set other non-required properties ...

    }
}

用于构建实体的相应 DTO(在存储库中创建,或来自 UI 的应用程序服务,或来自 WebService 等):

public class PersonDTO
{
    public long? ID { get; set; }
    public string Name { get; set; }
    public DateTime? DOB { get; set; }
}

请注意,我真的希望能够让 DTO 只是一个数据包(这基本上是 DTO 的全部,对吗?)。如果我在这里不允许可空类型,那么我必须在其他地方进行验证,重点是让实体中的值对象完成工作(对吗?)。

最后,这是我目前 RequiredField<T> class 上的内容。请注意,此代码无法编译。

public class RequiredField<T>
{
    private T _value;
    public T Value
    {
        get { return _value; }
        set
        {
            // handle special case of empty string:
            if (typeof(T) == typeof(string) && string.IsNullOrWhiteSpace((string)value))
            // but (string)value doesn't work: "Can't convert type 'T' to 'string'"
            {
                throw new ArgumentException("A required string must be supplied.");
            }
            else if (value == null)
            {
                throw new ArgumentException("A required field must be supplied.");
            }
            // also need to handle Nullable<T>, but can't figure out how
            else if (Nullable.GetUnderlyingType(typeof(T)) != null)
            // need to check value, not T
            {
                throw new ArgumentException("A required field must be supplied.");
            }
            _value = value;
        }
    }
    public RequiredField(T value)
    {
        Value = value;
    }
    // below is the start of failed attempt to accept a Nullable<T>
    // don't like the fact that I have validation going on here AND in the setter
    public RequiredField(object value)
    {
        if (!value.HasValue)
        {
            throw new ArgumentException("A required field must be supplied.");
        }
        Value = value.Value;
    }
}

所以我把自己搞得一团糟,我开始怀疑我是否在尝试做正确的事情。但是,如果我有一个好的开始,是什么让我冲过终点线?

我会建议利用现有的方法来验证输入,而不是滚动您自己的 RequiredValue 类型。

部分选项:

  1. 构造函数中的保护子句。如果您想在这里提供帮助,可以使用图书馆,即liteguard
  2. 基于属性 - 'Required' 属性,即类似于 DataAnnotations
  3. 更复杂的逻辑可以用FluentValidation
  4. 之类的东西封装

好吧,感谢一些正确方向的推动,以及 this 解决方案的帮助,我想出了自己的解决方案。它可能更漂亮一些,但它完成了工作(到目前为止通过了我所有的单元测试!)。

我最终不得不将输入值装箱。如果有一种不用装箱的方法,我当然会对更清洁的解决方案感兴趣。

public class RequiredField<T>
{
    private T _value;

    public RequiredField(IConvertible value)
    {
        SetValue(value);
    }

    public T GetValue()
    {
        return _value;
    }

    public void SetValue(IConvertible value)
    {
        Type t = typeof(T);
        Type u = Nullable.GetUnderlyingType(t);

        if (value == null)
        {
            // reference object is null
            throw new ArgumentException("A required field must be supplied.");
        }
        else if (value is string && string.IsNullOrWhiteSpace(Convert.ToString(value)))
        {
            // string is null or empty or whitespace
            throw new ArgumentException("A required field must be supplied.");
        }
        else if (u != null)
        {
            if (value == null)
            {
                // Nullable object is null
                throw new ArgumentException("A required field must be supplied.");
            }
            else
            {
                // Nullable object has value
                _value = (T)Convert.ChangeType(value, u);
            }
        }
        else
        {
            // value object is not null
            _value = (T)Convert.ChangeType(value, t);
        }
    }
}

I started to question if I'm attempting to do the right thing here.

很好,你应该质疑——文献建议你走另一条路。

I'm trying to create a Required Field Value Object class that will be reusable across Entities in my Domain Model.

这可能是错误的目标。

Evans Chapter 5 describes a number of tactical patterns for expressing the domain model, including the ValueObject 模式。 关键模式中的洞察力是,重要的是您的软件描述值代表什么,而不是它如何在内存中实现。

public RequiredField<DateTime> DOB { get; private set; }

所以这个声明试图告诉我们这个字段是这个实体的查询 api 的一部分,这个值是必需的,在内存中状态是一个数据结构的句柄,它支持日期时间 api.

缺少的是数据是出生日期。

这里有几个问题 -- 首先,RequiredField 不是从通用语言中提取的;它是人为的编程词汇,对您的领域专家来说毫无意义。

此外,它无法正确地对 DateOfBirth 进行建模(想想出生日期是什么——一个本地日期,由该人出生地的时钟测量)。 DateOfBirth 的时间算法不起作用。

除其他外,这意味着您要避免将 DateOfBirth 与其他时间混淆,例如日期算术有效的时间。

所以你的构造函数应该看起来像

public PersonEntity(PersonDTO dto)
{
    ID = new Identifier(dto.EY);
    Name = new Name(dto.EID);
    DOB = new DateOfBirth(dto.DOB);
    // set other non-required properties ...
}

这为我们提供了一个放置数据验证的自然位置(在值类型的构造函数中)

此外,当您在 模型中时,您可能想要标记可选字段,而不是显式字段。比较C#和Java.

中Optional的用法

用另一种方式拼写这一点,RequiredField 是一个 algebraic data type,大致对应于 Unit -- 您已经创建了一种只能采用一种类型的类型。

messaging 中,您更有可能默认需要 "optional" 字段,因为 forwards/backwards 与其他实现兼容的灵活性很有价值。您希望能够读取由过去版本的模型编写的消息,以及写入将由未来版本的模型读取的消息。

相同的想法,不同的拼写 -- 边界上的关注点与模型中的不同

实际上,归结为这个;模型中的状态受到约束,但约束存在于模型本身中——一旦从模型中提取状态(一旦创建 DTO),约束就消失了。数据只是一个字节数组;读回数据,我们 re-apply 约束,这样模型就不必不断地检查(换句话说,DRY 原则出现在这里)。

the pragmatist in me didn't want to create a zillion different value objects when most of them just need to be required, and have no extra validation or behavior.

即使没有验证,即使没有 "extra" 验证,将类型替换为其他类型仍然是错误的事实,就业务而言——我们可以同时表示FamilyName 和 City 为字符串,但这意味着它们可以互换,但事实并非如此。

换句话说,没有人会说 int,strings,天啊,strings 有编码,太复杂了,我就把所有东西都建模成 byte[]。

另见

也就是说,错误的代价可能不会超过正确的工作。编写样板代码 不好玩 ,您可能需要使用更适合该任务的语言编写您的值类型和基元到值的转换。取舍比比皆是。

So my take-a-way from this is that I should define separate VOs, and have each of those use a RequiredField helper (or perhaps FluentValidation)? This would also make it easy to add differing validations or behaviors to individual VOs.

常用成语

// constructors
new Value(...)

// factory methods
Value.of(...)
Value.from(...)

// Factories
api.newInstance(...)

// Builder
api.newBuilder()....build()

If that property is simply a description with no role in the domain logic/decisions, can it be a primitive and not a VO?

注意:如果 属性 在域 logic/decisions 中没有作用,为什么要包含它?

可以,是的,但确实不应该。值类型是您用于为业务建模的领域特定语言的语料库。换句话说,域行为不应该完全取决于数据在内存中的表示方式。

考虑身份;它们是不透明的值类型。你所做的就是比较它们。模型绝对没有理由需要透过面纱窥视它们是否具有相同的底层数据布局。

但即使我有

interface OpaqueValue extends ICanEqual {...}

我还是想要

interface Whatzit {
    interface Identity extends OpaqueValue {...}
}

interface Whoozit {
    interface Identity extends OpaqueValue {...}
}

// CompileTimeError
Whatzit.Identifier target = source.getWhatzit().id