我正在尝试创建一个必需字段值对象类,该类将在我的域模型中的实体之间可重用。我还在学习C#语法(一直在VB.net中编码)。我是DDD的新手(但至少读过几本书)。
我的目标是设计一个名为RequiredField<T>
的值对象,它可以接受几乎任何对象(例如值类型、引用类型或可为null的值类型(int、string、int?)),然后在允许它成为实体状态的一部分之前对其进行验证。每当我的实体有一个必需的字段(例如id、主键、名称或任何其他被认为是拥有有效实体所必需的状态)时,我都会使用这个值对象。因此,每当我有一段简单的数据需要一个有效的Entity时,Entity就会将属性定义为RequiredField<T>
。
所以我一直在摆弄这门课,而且我离它越来越近了,但似乎每次我认为我差不多能上的时候,我都会遇到另一个绊脚石。以下是我希望使用的样子,来自我的实体类:
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的全部内容,对吧?)。如果我在这里不允许可以为null的类型,那么我必须在其他地方进行验证,关键是让实体中的Value对象来完成这项工作(对吗?)。
最后,以下是到目前为止我在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;
}
}
所以我把自己弄得一团糟,我开始质疑我是否试图在这里做正确的事情。但如果我有一个好的开始,是什么让我冲过终点线?
我开始怀疑我是否试图在这里做正确的事情。
很好,你应该对此提出质疑——文献建议你走另一条路。
我正在尝试创建一个Required Field Value Object类,该类将在我的域模型中的实体之间可重用。
这可能是一个错误的目标。
Evans第5章描述了许多用于表示领域模型的战术模式,包括ValueObject模式。模式中的关键见解是,重要的是您的软件描述值所代表的内容,而不是如何在内存中实现它。
public RequiredField<DateTime> DOB { get; private set; }
因此,这个声明试图告诉我们,这个字段是这个实体的查询api的一部分,值是必需的,在内存中,状态是支持DateTimeapi的数据结构的句柄。
缺少的是数据是出生日期。
这里有几个问题——首先,RequiredField不是从无处不在的语言中提取的;这是人工编程词汇,对你的领域专家来说毫无意义。
此外,它未能正确建模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的用法进行比较。
- http://blog.joda.org/2015/08/java-se-8-optional-pragmatic-approach.html
- http://dolszewski.com/java/java-8-optional-use-cases/
换句话说,RequiredField
是一种代数数据类型,大致对应于Unit
——您创建了一种只能假设一种类型的类型。
在消息传递中,默认情况下您更可能想要"可选"字段,因为与其他实现前向/后向兼容的灵活性很有价值。您希望能够读取由模型的过去版本编写的消息,并编写将由模型的未来版本读取的消息。
相同的想法,不同的拼写——边界处的关注点与模型中的关注点不同
- http://blog.ploeh.dk/2011/05/31/AttheBoundaries,应用程序不面向对象/
- Gary Bernhardt谈边界
实际上,归根结底是这样;模型中的状态是受约束的,但约束存在于模型本身中——一旦从模型中提取了状态(一旦创建了DTO),约束就消失了。数据只是一个字节数组;读取数据后,我们重新应用约束,这样模型就不必不断地进行检查(换句话说,DRY原理显示在这里)。
我的实用主义者不想创建无数不同的值对象,因为大多数值对象都是必需的,并且没有额外的验证或行为。
即使没有验证,即使没有"额外"验证,就业务而言,用类型替换其他类型仍然是一个错误——我们可以将FamilyName和City表示为字符串,但这意味着它们是可互换的,但事实并非如此。
换句话说,没有人会说int,字符串,哦,我的天哪,字符串有编码,这太复杂了,我会把所有东西都建模为byte[]。
另请参见
- https://sourcemaking.com/refactoring/smells/primitive-obsession
- http://blog.ploeh.dk/2011/05/25/DesignSmellPrimitiveObsession/
- http://blog.ploeh.dk/2015/01/19/from-primitive-obsession-to-domain-modelling/
也就是说,犯错的代价可能不会超过做对的工作。编写锅炉板代码并不有趣,您可能需要用更适合任务的语言编写值类型和基元到值的转换。交易比比皆是。
所以我的方法是,我应该定义单独的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
我建议利用现有的验证输入的方法,而不是滚动自己的RequiredValue类型。
一些选项:
- 构造函数中的保护子句。如果你想在这里帮忙,你可以使用图书馆
- 基于属性-"必需"属性,例如类似DataAnnotations的属性
- 更复杂的逻辑可以用FluentValidation之类的东西封装
好吧,多亏了一些朝着正确方向的推动,以及这个解决方案的帮助,我想出了自己的解决方案。它可能会更漂亮一点,但它完成了任务(到目前为止通过了我所有的单元测试!)。
最后我不得不把输入值框起来。如果有一种不用装箱的方法,我肯定仍然对更清洁的解决方案感兴趣。
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);
}
}
}