我已经阅读了一些关于Getters和Setter使用的文章/帖子,以及它们如何有助于破解域模型对象中封装的目的。我理解不使用setter背后的逻辑 - 你允许客户端代码在对象业务规则和不变量的上下文之外操纵该对象的属性。
现在这位校长仍然困惑我。例如,如果我需要更改对象的成员变量的值,会发生什么?例如,如果某个人的姓名发生变化,我该如何在模型中反映出来?起初我想,为什么没有一个名为'ChangeName'的函数,让我传入新名称,然后它可以改变内部'name'变量。嗯....这只是一个制定者不是它!
我需要澄清的是 - 如果我要完全消除setter,那么在上述情况下,我是否应该完全依赖构造函数参数?我是否应该通过构造函数传递新属性值来代替旧属性值,之后我可以通过将对象传递给我拥有的任何持久性基础架构来持久化更改?
这两篇文章在本次讨论中很有用:
答案 0 :(得分:9)
嗯,这是一个经典的讨论。 Stack Overflow中还有其他一些关于此的线程。
但是。获取/设置(自动属性?)并不全是坏事。但是它们倾向于使您将实体构建为“死”数据容器,只有prop而不是方法。这种迹象通常被称为贫血领域 - 并且行为很少。我的建议是:
这里还有更多要说的......但答案很简短。 关于在构造函数中设置数据:如果没有该数据,此实体无法“生存”/存在,我只会这样做。对于实体人我会说Name可能不是那么重要。但社会安全号码可能是构造函数数据的候选者。或者实体员工必须在构造函数中拥有公司,仅仅因为员工必须属于公司。
答案 1 :(得分: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
,则可以绕过业务逻辑。但如果你足够自律,我认为这不是问题。对某些人来说这可能看起来很可怕,我理解。
从好的方面来说,如果您使用的是实体框架之类的东西,我认为您可以将其配置为通过调用属性(而不是支持字段)来吸收新实例,从而防止从数据库加载无效的聚合(假设您重新导入了一些可能包含一些垃圾的批量数据)。不过我还没有测试过。
在第二个示例中使用表达式体访问器只是为了表明您可以大量减少样板。
可以使用像上面这样的表达式主体 getter,因为字符串在 C# 中具有值语义,因此表达式 返回 _name
的副本,因此不会暴露对内部变量的引用。
请注意,以 C# 9 记录为例,您只有基于值的相等语义。一条记录仍然是通过引用传递的!由于理想情况下记录应该是不可变的(仅限 init),因此您可以返回对此类记录的引用(这样性能更高),而不必进行克隆(这对于浅克隆来说很简单,但对于深克隆来说却很困难)。
如果在聚合中有这样的对象,例如不是不可变记录的 DDD 值对象或可以轻松克隆的记录,则需要确保没有返回对内部的引用可以变异的对象,从而绕过业务逻辑并破坏聚合完整性。
以列表为例。您可以使用 IReadOnlyList
作为返回类型,但如果您仅转换私有内部属性,这还不够,因为该引用可以在外部“向上转换”为 List,然后用于修改它.
在这种情况下,您还应该使用 .AsReadOnly()
的 List
方法在原始列表的元素上返回一个新的只读包装器列表。
请注意,只有包装器列表不受更改(它没有添加或删除方法),但元素本身不受保护。他们有责任保护自己免受变化的影响。
编辑:
我刚刚意识到我的例子并不完全正确。此类(私有?)具有逻辑的访问器可用于简单逻辑,例如确保在设置结束日期时不在开始日期之前,但对于复杂情况,例如设置结束日期可能出于多种原因必须全部建模为动词,例如 terminateContract(DateTime finalDay, string reason)
或 closeContract(DateTime closedEarlyDate)
,它们必须更明确地说明设置结束日期的原因。无论如何,在这种情况下,应该始终应用的通用逻辑可以存在于 setter 访问器中(这提供了代码的重复数据删除),并且每个案例每个操作的逻辑可以存在于特定的操作方法中。