最近一直在看一些 Greg Young 的视频,我试图理解为什么对域对象上的 Setter 持消极态度。我认为域对象应该是 DDD 中的“重”逻辑。网上有没有关于坏例子的好例子,然后他们有办法纠正它?任何例子或解释都是好的。这仅适用于以 CQRS 方式存储的事件还是适用于所有 DDD?
7 回答
我正在贡献这个答案来补充 Roger Alsing 关于不变量的其他原因的答案。
语义信息
Roger 清楚地解释了属性设置器不携带语义信息。允许像 Post.PublishDate 这样的属性使用 setter 会增加混乱,因为我们无法确定帖子是否已发布,或者仅知道发布日期是否已设置。我们不能确定这就是发表一篇文章所需要的全部内容。对象“界面”并没有清楚地显示其“意图”。
不过,我相信像“启用”这样的属性确实为“获取”和“设置”提供了足够的语义。它应该立即生效,我看不出需要 SetActive() 或 Activate()/Deactivate() 方法,仅因为属性设置器上缺少语义。
不变量
Roger 还谈到了通过属性设置器打破不变量的可能性。这是绝对正确的,应该使属性协同工作以提供“组合不变值”(作为使用 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;
}
}
}
这是导致访问顺序依赖的“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),
};
}
由于我们无法更改期间对象的结束日期之后的开始日期(除非它是“空”期间,两个日期都设置为默认值(DateTime) - 是的,这不是一个很好的设计,但你得到了我意思是...)尝试首先设置开始日期将引发异常。
当我们使用对象初始化器时,它变得更加严重。由于 C# 不保证任何赋值顺序,我们无法做出任何安全假设,代码可能会或可能不会抛出异常,具体取决于编译器的选择。坏的!
这最终是类的设计问题。由于该属性无法“知道”您正在更新这两个值,因此它无法“关闭”验证,直到两个值都实际更改为止。您应该将两个属性设为只读并提供同时设置这两个属性的方法(失去对象初始化器的功能)或完全从属性中删除验证代码(可能引入另一个只读属性,如 IsValid,或验证随时需要)。
ORM“补水”*
水化,在一个简单的视图中,意味着将持久化的数据返回到对象中。对我来说,这确实是在属性设置器后面添加逻辑的最大问题。
许多/大多数 ORM 将持久值映射到属性中。如果您有验证逻辑或更改属性设置器内的对象状态(其他成员)的逻辑,您最终将尝试针对“不完整”对象(仍在加载的对象)进行验证。这与对象初始化问题非常相似,因为您无法控制字段“水合”的顺序。
大多数 ORM 允许您将持久性映射到私有字段而不是属性,这将允许对象被水合,但如果您的验证逻辑主要位于属性设置器中,您可能必须在其他地方复制它以检查加载的对象是否有效或不。
由于许多 ORM 工具通过使用映射到字段的虚拟属性(或方法)来支持延迟加载(ORM 的一个基本方面!),因此 ORM 无法延迟加载映射到字段的对象。
结论
因此,最后,为了避免代码重复,让 ORM 尽可能发挥最佳性能,根据字段设置的顺序防止意外异常,明智的做法是将逻辑从属性设置器中移开。
我仍在弄清楚这个“验证”逻辑应该在哪里。我们在哪里验证对象的不变性方面?我们在哪里放置更高级别的验证?我们是否在 ORM 上使用钩子来执行验证(OnSave、OnDelete、...)?等等等等等等,但这不是这个答案的范围。
Setter 不携带任何语义信息。
例如
blogpost.PublishDate = DateTime.Now;
这是否意味着该帖子已发布?或者只是发布日期已经确定?
考虑:
blogpost.Publish();
这清楚地表明了应该发布博客文章的意图。
此外,setter 可能会破坏对象不变量。例如,如果我们有一个“三角形”实体,则不变量应该是所有角度的总和应该是 180 度。
Assert.AreEqual (t.A + t.B + t.C ,180);
现在,如果我们有 setter,我们可以轻松打破不变量:
t.A = 0;
t.B = 0;
t.C = 0;
所以我们有一个三角形,其中所有角度的总和为0……那真的是三角形吗?我会说不。
用方法替换 setter 可能会迫使我们保持不变量:
t.SetAngles(0,0,0); //should fail
此类调用应引发异常,告诉您这会导致您的实体处于无效状态。
因此,您可以使用方法而不是 setter 获得语义和不变量。
这背后的原因是实体本身应该负责改变其状态。没有任何理由需要在实体本身之外的任何地方设置属性。
一个简单的例子是一个有名字的实体。如果您有一个公共设置器,您将能够从应用程序中的任何位置更改实体的名称。如果您改为删除该设置器并ChangeName(string name)
为您的实体放置一个类似的方法,这将是更改名称的唯一方法。这样,您可以添加任何在更改名称时始终运行的逻辑,因为只有一种方法可以更改它。这也比将名称设置为更清晰。
基本上,这意味着您在私下保留状态的同时公开暴露您的实体上的行为。
原始问题标记为 .net,因此我将针对您希望将实体直接绑定到视图的上下文提交一种实用的方法。
我知道这是不好的做法,你可能应该有一个视图模型(如在 MVVM 中)或类似的东西,但对于一些小型应用程序,恕我直言,不要过度模式化是有意义的。
使用属性是 .net 中开箱即用的数据绑定方式。也许上下文规定数据绑定应该双向工作,因此实现 INotifyPropertyChanged 并将 PropertyChanged 作为设置器逻辑的一部分是有意义的。
当客户端设置无效值时,实体可以例如将项目添加到损坏的规则集合等(我知道 CSLA 几年前就有这个概念,也许现在仍然如此),并且该集合可以显示在 UI 中。工作单元稍后会拒绝持久化无效对象,如果它应该达到那么远。
我试图在很大程度上证明解耦、不变性等是合理的。我只是说在某些情况下需要更简单的架构。
setter 只是设置一个值。它不应该是"heavy" with logic
。
具有良好描述性名称的对象上的方法应该是那些"heavy" with logic
并且在域本身中有类似的方法。
我强烈推荐阅读Eric Evans 的 DDD 书和Bertrand Meyer 的 Object-Oriented Software Construction。他们拥有您需要的所有样品。
我可能不在这里,但我认为应该使用 setter 来设置,而不是 setter 方法。我有几个原因。
a) 这在 .Net 中是有意义的。每个开发人员都知道属性。这就是您在对象上设置事物的方式。为什么要偏离域对象?
b) Setter 可以有代码。我相信在 3.5 之前,设置一个对象由一个内部变量和一个属性签名组成
private object _someProperty = null;
public object SomeProperty{
get { return _someProperty; }
set { _someProperty = value; }
}
将验证放在 setter 中非常简单且优雅。在 IL 中,getter 和 setter 无论如何都会转换为方法。为什么要重复代码?
在上面发布的 Publish() 方法的示例中,我完全同意。有时我们不希望其他开发人员设置属性。这应该通过一种方法来处理。但是,当 .Net 已经在属性声明中提供了我们需要的所有功能时,为每个属性设置一个 setter 方法是否有意义?
如果你有一个 Person 对象,如果没有理由,为什么要为它的每个属性创建方法?