【问题标题】:DDD and the use of Getters and SettersDDD 和 Getter 和 Setter 的使用
【发布时间】:2012-01-07 15:11:39
【问题描述】:

我已经阅读了一些关于 Getter 和 Setter 使用的文章/帖子,以及它们如何帮助打破在域模型对象中封装的目的。我理解不使用 setter 背后的逻辑 - 您允许客户端代码在对象业务规则和不变量的上下文之外操作该对象的属性。

现在这个校长仍然让我感到困惑。例如,如果我需要更改对象的成员变量的值会发生什么?例如,如果一个人的名字发生了变化,我如何在模型中反映这一点?起初我想,为什么不使用一个名为“ChangeName”的函数,让我传入新名称,然后它可以更改内部的“名称”变量。嗯....这只是一个二传手不是吗!

我需要澄清一下 - 如果我要完全消除 setter,那么在上述情况下,我应该只依赖构造函数参数吗?我是否应该通过构造函数传递新的属性值来代替旧的属性值,然后我可以通过将对象传递到我拥有的任何持久性基础设施来持久化更改?

这两篇文章在这个讨论中很有用:

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

【问题讨论】:

    标签: domain-driven-design encapsulation setter getter getter-setter


    【解决方案1】:

    我认为我们应该看看 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,你可以绕过业务逻辑。但如果你足够自律,我认为这不是问题。不过,这对某些人来说可能看起来很可怕,我理解。

    从好的方面来说,如果您使用的是实体框架之类的东西,我认为您可以将其配置为通过调用属性(而不是支持字段)来填充新实例,从而防止从数据库加载无效的聚合(例如,如果您重新导入一些可能包含一些垃圾的批量数据)。不过我还没有测试过。

    在第二个示例中使用表达式体访问器只是为了表明您可以大量减少样板代码。

    可以使用像上面这样的表达式实体 getter,因为字符串在 C# 中具有值语义,因此表达式返回 _name 的副本,因此不会公开对内部变量的引用。

    请注意,以 C# 9 记录为例,您只有基于值的相等语义。一条记录还是通过引用传递的!由于理想情况下记录应该是不可变的(仅限 init),因此您可以返回对此类记录的引用(性能更高)并跳过克隆(这对于浅层克隆很简单,但对于深层克隆很难)。

    如果您在聚合中有这样的对象,例如不是不可变记录或可以轻松克隆的记录的 DDD 值对象,您需要确保没有返回对内部的引用可以变异的对象,从而绕过业务逻辑并破坏聚合完整性。

    以列表为例。您可以使用IReadOnlyList 作为返回类型,但如果您只转换私有内部属性,这还不够,因为该引用可以在外部“向上转换”回列表,然后用于修改它。

    在这种情况下,您还应该使用List.AsReadOnly() 方法在原始列表的元素上返回一个新的只读包装器列表。

    请注意,虽然只有包装器列表受到保护(它没有添加或删除方法),但元素本身不受保护。保护自己免受变化是他们的责任。

    编辑

    我刚刚意识到我的例子并不完全正确。这种(私人?)具有逻辑的访问器可用于简单的逻辑,例如确保在设置结束日期时,它不在开始日期之前,但对于复杂的情况,设置结束日期可能有多种原因必须全部建模为动词,例如terminateContract(DateTime finalDay, string reason)closeContract(DateTime closedEarlyDate),它们必须更明确地说明设置结束日期的原因。无论如何,在这种情况下,应该始终应用的通用逻辑可以存在于 setter 访问器中(这提供了代码的重复数据删除),并且每个案例的每个操作逻辑可以存在于特定的操作方法中。

    【讨论】:

      【解决方案2】:

      嗯,这是一个经典的讨论。 Stack Overflow 中还有其他几个关于此的主题。

      但是。获取/设置(自动属性?)并不全是坏事。但它们往往会让您将实体构建为只有 prop 而没有方法的“死”数据容器。这种迹象通常被称为贫血域 - 并且几乎没有行为。我的建议是:

      1. 尽量少用道具。
      2. 尝试查找属于一起的数据组,并且应该是 像前任一样在一起。名字中间名和姓氏。另一个例子 是邮政编码,城市,街道。这些数据最好通过一个 方法。它可以最大限度地减少您的实体无效的机会。
      3. 通常属于一起的数据可以分组为一个值 对象。
      4. 更多的值对象往往会带来更多的描述性方法 来自您的实体是“动词”而不是您通常的“名词” 实体。
      5. 您的值对象的更多方法也可以添加更多 行为,也许会减少你的“胖”服务(也许你不 有太多泄露业务逻辑的服务...)。

      这里还有很多话要说……但要简短回答。 关于在构造函数中设置数据:如果没有该数据,该实体无法“生存”/存在,我只会这样做。对于实体 Person,我会说 Name 可能不是那么重要。但社会安全号码可能是构造函数数据的候选者。或者实体Employee必须在构造函数中有Company,仅仅因为员工必须属于公司。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-11-26
        • 1970-01-01
        • 2016-02-19
        • 2011-11-09
        • 1970-01-01
        • 2021-10-17
        • 2016-02-24
        • 2017-09-07
        相关资源
        最近更新 更多