我认为我们应该看看 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 访问器中(这提供了代码的重复数据删除),并且每个案例的每个操作逻辑可以存在于特定的操作方法中。