Bud*_*Joe 11 .net oop domain-driven-design domain-model cqrs
最近一直在观看一些Greg Young视频,我试图理解为什么对Setters on Domain对象持否定态度.我认为在DDD中,Domain对象应该是"重"的逻辑.这个坏例子在网上是否有任何好的例子然后他们纠正它的方法?任何例子或解释都是好的.这仅适用于以CQRS方式存储的事件,还是适用于所有DDD?
Lou*_*ier 11
我正在贡献这个答案,以补充Roger Alsing以其他原因回答不变量.
语义信息
罗杰清楚地解释说,财产制定者不携带语义信息.允许像Post.PublishDate这样的属性的setter可能会增加混乱,因为我们无法确定帖子是否已发布或仅在发布日期已设置时.我们无法确定这是发布文章所需的全部内容.对象"界面"没有清楚地显示其"意图".
不过,我相信像"启用"这样的属性确实为"获取"和"设置"提供了足够的语义.它应该是立即有效的,我不能看到需要SetActive()或Activate()/ Deactivate()方法,原因仅在于属性设置器上缺少语义.
不变
罗杰还谈到了通过财产制定者打破不变量的可能性.这是绝对正确的,并且应该使得能够串联工作的属性提供"组合不变值"(作为使用Roger示例的三角形的角度)作为只读属性并创建一个方法来将它们全部设置在一起,这可以在一个步骤中验证所有组合.
属性顺序初始化/设置依赖项
这类似于不变量的问题,因为它会导致应该一起验证/更改的属性出现问题.想象一下以下代码:
public class Period
{
DateTime start;
public DateTime Start
{
get { return start; }
set
{
if (value > end && end != default(DateTime))
throw new Exception("Start can't be later than end!");
start = value;
}
}
DateTime end;
public DateTime End
{
get { return end; }
set
{
if (value < start && start != default(DateTime))
throw new Exception("End can't be earlier than start!");
end = value;
}
}
}
Run Code Online (Sandbox Code Playgroud)
这是导致访问顺序依赖性的"setter"验证的简单示例.以下代码说明了此问题:
public void CanChangeStartAndEndInAnyOrder()
{
Period period = new Period(DateTime.Now, DateTime.Now);
period.Start = DateTime.Now.AddDays(1); //--> will throw exception here
period.End = DateTime.Now.AddDays(2);
// the following may throw an exception depending on the order the C# compiler
// assigns the properties.
period = new Period()
{
Start = DateTime.Now.AddDays(1),
End = DateTime.Now.AddDays(2),
};
// The order is not guaranteed by C#, so either way may throw an exception
period = new Period()
{
End = DateTime.Now.AddDays(2),
Start = DateTime.Now.AddDays(1),
};
}
Run Code Online (Sandbox Code Playgroud)
因为我们无法在句点对象上更改结束日期之后的开始日期(除非它是一个"空"句点,两个日期都设置为默认值(DateTime) - 是的,这不是一个很棒的设计,但你得到的是我意味着...)尝试先设置开始日期将引发异常.
当我们使用对象初始化器时,它会变得更加严重.由于C#不保证任何赋值顺序,我们不能做出任何安全假设,代码可能会也可能不会抛出异常,具体取决于编译器的选择.坏!
这最终是类的设计问题.由于该属性无法"知道"您正在更新这两个值,因此在实际更改这两个值之前,它无法"关闭"验证.您应该将两个属性设置为只读并提供同时设置两个属性的方法(丢失对象初始化程序的功能)或从属性中完全删除验证代码(可能引入另一个只读属性,如IsValid,或验证它需要的时候).
ORM"保湿"*
在简单化的视图中,水合意味着将持久化数据恢复到对象中.对我来说,这是添加属性设置器后面的逻辑的最大问题.
许多/大多数ORM将持久值映射到属性中.如果您具有更改属性设置器内的对象状态(其他成员)的验证逻辑或逻辑,您将最终尝试验证"不完整"对象(一个仍在加载).这与对象初始化问题非常相似,因为您无法控制字段"水合"的顺序.
大多数ORM允许您将持久性映射到私有字段而不是属性,这将允许对象被水合,但如果您的验证逻辑主要位于属性设置器中,您可能必须在其他位置复制它以检查加载的对象是否有效或不.
由于许多ORM工具通过使用映射到字段的虚拟属性(或方法)来支持延迟加载(ORM的一个基本方面!),因此ORM无法将延迟加载对象映射到字段中.
结论
因此,最后,为了避免代码重复,允许ORM尽可能地执行,根据字段设置的顺序防止出现意外异常,将逻辑从属性设置器移开是明智的.
我还在弄清楚这个'验证'逻辑应该在哪里.我们在哪里验证对象的不变量方面?我们在哪里进行更高级别的验证?我们是否在ORM上使用钩子来执行验证(OnSave,OnDelete,...)?等等等.但这不是这个答案的范围.
Rog*_*son 10
Setter不携带任何语义信息.
例如
blogpost.PublishDate = DateTime.Now;
Run Code Online (Sandbox Code Playgroud)
这是否意味着帖子已发布?或者只是已经设定了发布日期?
考虑:
blogpost.Publish();
Run Code Online (Sandbox Code Playgroud)
这清楚地表明了应该发布博客帖子的意图.
此外,setter可能会破坏对象不变量.例如,如果我们有一个"三角形"实体,则不变量应该是所有角度的总和应该是180度.
Assert.AreEqual (t.A + t.B + t.C ,180);
Run Code Online (Sandbox Code Playgroud)
现在,如果我们有setter,我们可以轻松打破不变量:
t.A = 0;
t.B = 0;
t.C = 0;
Run Code Online (Sandbox Code Playgroud)
所以我们有一个三角形,其中所有角度的总和为0 ......这真的是一个三角形吗?我会说不.
用方法替换setter可能会迫使我们维护不变量:
t.SetAngles(0,0,0); //should fail
Run Code Online (Sandbox Code Playgroud)
此类调用应抛出一个异常,告诉您这会导致您的实体出现无效状态.
因此,您可以使用方法而不是setter来获取语义和不变量.
这背后的原因是实体本身应该负责改变其状态.没有任何理由您需要在实体本身之外的任何地方设置属性.
一个简单的例子是具有名称的实体.如果您有公共设置器,则可以从应用程序的任何位置更改实体的名称.如果您改为移除该setter并将类似方法放入ChangeName(string name)您的实体,这将是更改名称的唯一方法.这样,您可以添加任何类型的逻辑,这些逻辑在您更改名称时将始终运行,因为只有一种方法可以更改它.这比将名称设置为某些东西要清楚得多.
基本上这意味着您在私下保持状态时公开公开您的实体的行为.