为什么我应该避免在域实体属性上使用 public setter

Why should I avoid having public setters on domain entity properties

我正在尝试将 DDD 应用到我正在开发的其中一个应用程序中,但我不能说我已经 100% 掌握了它。

在我查看的大多数示例中,似乎我们都在努力避免在域实体属性上使用 public setter。例如,我看到如下实现的域实体:

public class Product
{
    public Product(string name)
    {
        if (name == null)
        {
            throw new ArgumentNullException("name");
        }

        Name = name;
    }

    public string Name { get; private set; }

    public void UpdateName(string newName)
    {
        if (newName == null)
        {
            throw new ArgumentNullException("newName");
        }

        Name = newName;
        DomainEvents.Raise(new ProductNameUpdatedEvent(this));
    }
}

用法类似于:

// have a Product object instance here somehow
product.UpdateName("foobar");

但是,我可以通过在 Name 属性 的 setter 上实现更新逻辑来实现相同的行为,如下所示:

public class Product
{
    private string _name;

    public Product(string name)
    {
        if (name == null)
        {
            throw new ArgumentNullException("name");
        }

        _name = name;
    }

    public string Name
    {
        get
        {
            return _name;
        }

        set
        {
            if (value == null)
            {
                throw new ArgumentNullException("value");
            }

            _name = value;
            DomainEvents.Raise(new ProductNameUpdatedEvent(this));
        }
    }
}

在这种情况下,用法如下所示:

// have a Product object instance here somehow
product.Name = "foobar";

我可以看到第一个实际上更好地表达了意图,但我看不出这里有太大差异。除了更好的表现力之外,还有什么其他原因让您倾向于第一种选择?在 DDD 中,选择第二个选项是不是一个很大的禁忌?

DDD 是关于无处不在的语言。 CRUD(Create/Read/Update/Delete) 术语通常不会出现在无处不在的语言中。从企业的角度来看,您是说产品名称可以更新还是产品可以重命名

我不能确定,因为我不知道你的域,但 rename 可能更合适。

现在,假设您看到这行代码 product.name = 'some name',您认为它的真正含义是什么?你必须依赖这样的知识,即 Product 不能在没有名称的情况下构造,以确保我们实际上正在重命名它。否则,这也可能意味着我们正在命名最初未命名的产品。

product.rename(...),明明是用意,没有解释的余地​​。它也更符合真正重要的无处不在的语言。

此外,也许企业想要了解为什么更改了产品名称?是不是因为错别字而改了?是否出于营销原因而更改?您将如何使用简单的 setter?

来捕获它?

我不是很了解 C#,所以这可能不是惯用的,但下面的内容似乎更有意义:

public class Product {

    //Note: Using ProductName rather than string would be helpful
    //to protect further invariants (e.g. name cannot be empty string)
    private string _name;

    public Product(string name) {
        Name = name;
    }

    public string Name {
        get {
            return _name;
        }

        private set {
            if (value == null) {
                throw new ArgumentNullException("A product must be named");
            }

            _name = value;
        }
    }

    public void rename(string newName) {
        string oldName = Name;

        Name = newName;

        //Assume the entity has the supporting code for a  unique identifier
        //productId
        DomainEvents.Raise(new ProductRenamed(productId, oldName, newName));
    }
}

另请注意,我使用其他相关信息增强了域事件。现在,处理事件时还有一件重要的事情是将它们写入同一事务中的磁盘,在该事务中聚合被修改。事务提交后,它们可以通过消息传递基础设施发布以供进一步处理。一般情况下,您希望避免在事务实际提交之前发生副作用。

由于您使用的是域事件,因此您的问题会更有趣一些。

我在某个时候遇到了以下 post:http://www.jayway.com/2013/06/20/dont-publish-domain-events-return-them/

所以这里的想法是不要使用一些任意的全局可访问的基础设施来发布域事件,而是简单地return作为操作结果的事件。

出于以下几个原因,我觉得这个选项很有趣:

  • 不依赖所有客户端都需要使用的某些 DomainEvents 静态实现。
  • 测试更容易,因为我们不需要连接到静态单例 DomainEvents

不过,使用您的示例会增加另一个维度,因为在这种情况下甚至无法使用 setter,因为它不能 return 域事件结果。所以当使用域事件时,它可能会更好地改变一个人的设计,我希望 :)