DDD和Getter和Setter的使用

16

我阅读了几篇关于Getters和Setters使用的文章/帖子,以及它们如何有助于打破领域模型对象的封装性的文章。我理解不使用Setter的逻辑 - 您允许客户端代码在对象业务规则和不变量的上下文之外操作该对象的属性。

现在这个原则仍然让我困惑。例如,如果我需要更改对象的成员变量的值会发生什么?例如,如果一个人的名字更改了,我该如何在模型中反映这一点?起初,我想,为什么不有一个名为“ChangeName”的函数,让我传入新名称,它可以相应地更改内部的“name”变量。但是....那不就是一个Setter吗!

我需要澄清的是 - 如果我要完全消除Setter,在上述情况下,我是否应该仅依赖构造函数参数?我应该通过构造函数传递新属性值来替换旧属性值,之后我可以通过将对象传递给任何持久化基础结构来保存更改吗?

以下两篇文章对此讨论很有用:

  1. http://kellabyte.com/tag/ddd/
  2. http://typicalprogrammer.com/?p=23
2个回答

13

嗯,这是一个经典的讨论。这里在 Stack Overflow 上有几个其他线程讨论这个。

但是 Get/Set(自动属性?)并不全是坏的。但它们倾向于使您将实体构造为“死”的数据容器,只拥有属性而没有方法。这种迹象通常被称为贫血领域,几乎没有行为。我的建议是:

  1. 尽量少使用属性。
  2. 尝试找到应该放在一起的数据组,比如名字、中间名和姓氏。另一个例子是邮政编码、城市、街道。这些数据最好通过方法设置。它可以最小化实体无效的机会。
  3. 通常属于一起的数据可以作为值对象分组。
  4. 更多的值对象往往会从您的实体中带出更多描述性的方法,这些方法是“动词”,而不是通常的“名词”实体。
  5. 值对象的更多方法也可以打开添加更多行为的可能性,也许还可以减少您的“肥”的服务(也许您没有过多泄漏业务逻辑的服务...)。

这里还有更多要说...但是简短的答案。 关于在构造函数中设置数据:只有在没有这些数据实体无法“生存”/存在时,我才会这样做。对于 Person 实体,我会说 Name 可能不是那种重要的属性。但社会安全号码可能是构造函数数据的候选项。或者实体 Employee 必须在构造函数中拥有 Company,因为雇员必须属于公司。


0

我认为我们应该看一下DDD的原则,并从中得出正确的答案。

C#中的公共自动属性getter/setter在功能上只是公共属性。只要没有关于相应属性的正确值的业务规则和不需要在这些属性更改时触发领域事件,使用自动属性getter/setter并不本质上是不好的。

此外,一个不仅仅包含公共自动属性的聚合或实体会导致贫血模型和贫血领域。这样的“聚合”不是真正的聚合,而更像是DTO或值对象。

个人认为,如果我们使用具有主体的属性访问器(get/set)来集成业务逻辑,我们可以使我们的代码更易读,可能也更简洁。

例如:

// Instead of this:

public DemoAggregate : IAggregate
{
  public string Name { get; private set; }

  public void ChangeName(string newName)
  {
    Name = Check.MinMaxLength(newName, 1, 100,
      $"{nameof(newName)} length must be between 1 and 100 characters.");
  }
}

/* MinMaxLength throws a business exception
 * if the new name is outside of the accepted range
 * otherwise it returns the value unchanged.
 */

// ...you can write this to get one method less:

public AltDemoAggregate : IAggregate
{
  private string _name;

  public string Name
  {
    get => _name;
    set => value = Check.MinMaxLength(newName, 1, 100,
      $"{nameof(newName)} length must be between 1 and 100 characters.");
  }
}

上述方法的唯一问题是,如果在某些方法中直接设置_name,则可以绕过业务逻辑。但是,如果你足够有纪律性,我认为这不是一个问题。对于一些人来说可能会感到害怕,我理解这一点。
好处是,如果你使用类似Entity Framework的东西,我认为你可以配置它通过调用属性(而不是后备字段)来填充新实例,从而防止从数据库加载无效的聚合(例如,如果你导入了一些可能包含垃圾数据的批量数据)。不过我还没有测试过。
第二个示例中使用表达式主体访问器只是为了显示可以大大减少样板文件。
由于C#中字符串具有值语义,因此可以使用像上面那样的表达式主体getter,因此该表达式返回_name的副本,因此不会公开对内部变量的引用。
请注意,例如使用C# 9记录时,你只有基于值的相等语义。记录仍然通过引用传递!由于记录应该是不可变的(仅限init),因此你可以返回对这些记录的引用(这更高效),并跳过克隆(对于浅克隆很简单,但对于深克隆很困难)。
如果您在聚合内部拥有这样一个对象,例如不是不可变记录或可以轻松克隆的 DDD 值对象,则需要确保您未返回对可突变的内部对象的引用,从而绕过业务逻辑并干扰聚合完整性。
以列表为例。您可以使用 IReadOnlyList 作为返回类型,但如果您只将私有内部属性进行强制转换,那么仅此还不够,因为该引用可以“向上转型”回到 List 并用于修改它。
在这种情况下,您还应使用 List.AsReadOnly() 方法,返回一个新的只读包装器列表,覆盖原始列表中的元素。
请注意,仅包装器列表受到保护(它没有添加或删除方法),而不是元素本身。它们自己负责保护自己免受更改。 编辑: 我刚意识到我的例子并不完全正确。这样的(私有?)访问器与逻辑可以用于简单的逻辑,例如确保在设置结束日期时,它不会早于开始日期,但对于复杂情况,例如设置结束日期可能会出现多种原因,这些原因必须全部建模为动词,例如terminateContract(DateTime finalDay, string reason)closeContract(DateTime closedEarlyDate),它们更明确地说明了设置结束日期的原因。无论如何,在这种情况下,应始终应用通用逻辑的setter访问器可以存在(这提供了代码去重),而每个操作的特定情况逻辑可以存在于特定的操作方法中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接