如何创建必填字段值对象

时间:2017-02-02 01:06:52

标签: c# domain-driven-design nullable value-objects

我正在尝试创建一个必填字段值对象类,该类可以在我的域模型中的实体之间重复使用。我还在学习C#语法(永远在VB.net中编码)。我对DDD都很陌生(但至少读过几本书)。

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

所以我一直在为这堂课摆弄,而且我越来越近了,但似乎每次我想我得到它,我都会遇到另一个绊脚石。以下是我希望从我的Entity类中看到的用法:

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>课上的内容。请注意,此代码无法编译。

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;
    }
}

所以我让自己陷入了混乱,我开始质疑我是否想在这里做正确的事情。但如果我有一个良好的开端,是什么让我超越了终点?

3 个答案:

答案 0 :(得分:3)

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

一些选项:

  1. 构造函数中的Guard子句。如果你想在这里帮忙,你可以使用图书馆。 liteguard
  2. 基于属性 - '必需'属性,即。像DataAnnotations
  3. 之类的东西
  4. 更复杂的逻辑可以用FluentValidation
  5. 之类的东西封装

答案 1 :(得分:3)

  

我开始质疑我是否试图在这里做正确的事。

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

  

我尝试创建一个必需字段值对象类,该类可以在我的域模型中的实体之间重复使用。

这可能是错误的目标。

Evans Chapter 5描述了表达域模型的一些战术模式,包括ValueObject模式。模式中的关键洞察力是,您的软件描述值所代表的内容非常重要,而不是它在内存中的实现方式。

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

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

缺少的是数据是DateOfBirth。

这里有几个问题 - 首先,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 ...
}

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

此外,当您 in 模型时,您可能希望标记可选字段,而不是显式字段。将C#与Java中的Optional用法进行比较。

另一种方式拼写这一点,RequiredField是一个algebraic data type,大致对应Unit - 您已经创建了一种只能假设一种类型的类型。< / p>

消息中,您更可能需要&#34;可选&#34;默认情况下,因为向前/向后兼容其他实现的灵活性是有价值的。您希望能够读取模型的过去版本所写的消息,并编写将由模型的未来版本读取的消息。

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

实际上,归结为此;模型中的状态受到约束,但约束存在于模型本身中 - 一旦从模型中提取状态(一旦创建DTO),约束就消失了。数据只是一个字节数组;重新读取数据,我们重新应用约束,以便模型不必经常检查(换句话说,DRY原则显示在这里)。

  

我的实用主义者并不想创造大量不同的价值对象,只需要大多数对象,并且没有额外的验证或行为。

即使没有验证,即使没有&#34;额外&#34;验证,对于其他类型替换类型仍然是一个错误,就业务而言 - 我们可以将FamilyName和City都表示为字符串,但这意味着它们是可互换的,这不是在所有情况下。

换句话说,没有人理智说int,字符串,哦我的上帝字符串有编码,它太复杂了,我只是将所有内容建模为byte []。

另见

那就是说,错误的代价可能不会超过正确的工作。编写锅炉板代码并不有趣,您可能需要使用更适合该任务的语言编写您的值类型和原语以进行价值转换。权衡比比皆是。

  

所以我从中获取的方法是我应该定义单独的VO,并且每个VO都使用RequiredField帮助程序(或者可能是FluentValidation)?这也可以轻松地向各个VO添加不同的验证或行为。

常用习语

// constructors
new Value(...)

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

// Factories
api.newInstance(...)

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

如果该属性只是在域逻辑/决策中没有角色的描述,它可以是原始而不是VO吗?

注意:如果该属性在域逻辑/决策中没有任何作用,为什么要包括它?

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

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

但即使我有

interface OpaqueValue extends ICanEqual {...}

我仍然想要

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

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

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

答案 2 :(得分:1)

好的,由于正确方向的一些推动,以及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);
        }
    }
}