域驱动设计,域对象,关于Setters的态度

时间:2010-12-15 15:33:57

标签: .net domain-driven-design cqrs domain-model oop

最近一直在观看一些Greg Young视频,我试图理解为什么对Setters on Domain对象持否定态度。我认为在DDD中,Domain对象应该是“重”的逻辑。这个坏例子在网上是否有任何好的例子然后他们纠正它的方法?任何例子或解释都是好的。这仅适用于以CQRS方式存储的事件,还是适用于所有DDD?

7 个答案:

答案 0 :(得分: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;
            }
        }
    }

这是“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#不保证任何赋值顺序,我们不能做出任何安全假设,代码可能会也可能不会抛出异常,具体取决于编译器的选择。 BAD!

这最终是类的设计问题。由于该属性无法“知道”您正在更新这两个值,因此在实际更改这两个值之前,它无法“关闭”验证。您应该将两个属性设置为只读并提供同时设置两个属性的方法(丢失对象初始化程序的功能)或从属性中完全删除验证代码(可能引入另一个只读属性,如IsValid,或验证无论何时需要)。

ORM“保湿”*

水合作用,在简单的视图中,意味着将持久化数据恢复为对象。对我来说,这是添加属性设置器后面的逻辑的最大问题。

许多/大多数ORM将持久值映射到属性中。如果您具有更改属性设置器内的对象状态(其他成员)的验证逻辑或逻辑,您将最终尝试验证“不完整”对象(一个仍在加载)。这与对象初始化问题非常相似,因为您无法控制字段“水合”的顺序。

大多数ORM允许您将持久性映射到私有字段而不是属性,这将允许对象被水合,但如果您的验证逻辑主要位于属性设置器中,您可能必须在其他位置复制它以检查是否已加载对象是否有效。

由于许多ORM工具通过使用映射到字段的虚拟属性(或方法)来支持延迟加载(ORM的一个基本方面!),因此ORM无法将延迟加载对象映射到字段中。

结论

因此,最后,为了避免代码重复,允许ORM尽可能地执行,根据字段设置的顺序防止出现意外异常,将逻辑从属性设置器移开是明智的。

我还在弄清楚这个'验证'逻辑应该在哪里。我们在哪里验证对象的不变量方面?我们在哪里进行更高级别的验证?我们是否在ORM上使用钩子来执行验证(OnSave,OnDelete,...)?等等等。但这不是这个答案的范围。

答案 1 :(得分:10)

Setters不携带任何语义信息。

e.g。

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获得语义和不变量。

答案 2 :(得分:7)

背后的原因是实体本身应该负责改变其状态。没有任何理由您需要在实体本身之外的任何地方设置属性。

一个简单的例子是具有名称的实体。如果您有公共设置器,则可以从应用程序的任何位置更改实体的名称。如果您改为删除该setter并将ChangeName(string name)之类的方法添加到您的实体中,这将是更改名称的唯一方法。这样,您可以添加任何类型的逻辑,这些逻辑在您更改名称时将始终运行,因为只有一种方法可以更改它。这比将名称设置为某些东西要清楚得多。

基本上,这意味着您在私下保持状态的同时公开实体上的行为。

答案 3 :(得分:2)

原始问题标记为.net,因此我将针对您希望将实体直接绑定到视图的上下文提交实用方法。

我知道这是不好的做法,你应该有一个视图模型(如在MVVM中)或类似的东西,但是对于一些小应用程序来说,没有过度模式化IMHO是有意义的。

使用属性是开箱即用的数据绑定在.net中的工作方式。也许上下文规定数据绑定应该双向工作,因此实现INotifyPropertyChanged并将PropertyChanged作为setter逻辑的一部分提升是有道理的。

该实体可以例如在客户端设置无效值时,将项目添加到损坏的规则集合等(我知道CSLA在几年前就有这个概念,可能仍然存在),并且该集合可以在UI中显示。如果它应该到目前为止,工作单元后来会拒绝保留无效对象。

我试图在很大程度上证明解耦,不变性等等。我只是说在某些情况下需要更简单的架构。

答案 4 :(得分:1)

setter只是设置一个值。它不应该是"heavy" with logic

具有良好描述性名称的对象的方法应该是"heavy" with logic,并且在域本身中具有类似物。

答案 5 :(得分:1)

我强烈建议您阅读 DDD book by Eric Evans Object-Oriented Software Construction by Bertrand Meyer 。他们拥有您需要的所有样品。

答案 6 :(得分:-1)

我可能会离开这里,但我相信setter应该用来设置,而不是setter方法。我有几个原因。

a)在.Net中有意义。每个开发人员都知道属性。这就是你在一个物体上设置东西的方式。为什么偏离域对象呢?

b)制定者可以拥有代码。在3.5之前我认为,设置一个由内部变量和属性签名组成的对象

private object _someProperty = null;
public object SomeProperty{
    get { return _someProperty; }
    set { _someProperty = value; }
}

在验证器中放置验证非常容易且非常优雅。在IL中,无论如何都将getter和setter转换为方法。为什么重复代码?

在上面发布的Publish()方法的示例中,我完全同意。有时我们不希望其他开发者设置属性。这应该由一个方法来处理。但是,当.Net在属性声明中提供我们需要的所有功能时,为每个属性设置一个setter方法是否有意义?

如果你有一个Person对象,为什么没有理由为它上面的每个属性创建方法?