我认为我们应该看一下DDD的原则,并从中得出正确的答案。
C#中的公共自动属性getter/setter在功能上只是公共属性。只要没有关于相应属性的正确值的业务规则和不需要在这些属性更改时触发领域事件,使用自动属性getter/setter并不本质上是不好的。
此外,一个不仅仅包含公共自动属性的聚合或实体会导致贫血模型和贫血领域。这样的“聚合”不是真正的聚合,而更像是DTO或值对象。
个人认为,如果我们使用具有主体的属性访问器(get/set)来集成业务逻辑,我们可以使我们的代码更易读,可能也更简洁。
例如:
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.");
}
}
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访问器可以存在(这提供了代码去重),而每个操作的特定情况逻辑可以存在于特定的操作方法中。