实体最佳实践与规范
本文档提供基于领域驱动设计原则在模块与应用程序中实现聚合根及实体类的最佳实践。
请确保已优先阅读《实体》文档。
实体
每个聚合根同时也是一个实体。因此,除非聚合根规则另有规定,以下规则同样适用于聚合根。
- 务必在领域层中定义实体。
主构造函数
务必定义一个主构造函数,确保实体在创建时即处于有效状态。主构造函数用于由应用程序代码创建实体的新实例。
务必根据需求将主构造函数定义为
public、internal或protected internal。若非公开,则预期该实体将由领域服务创建。务必在主构造函数中初始化子集合。
请勿在构造函数内部生成
Guid键。应将其作为参数传入,以便调用代码使用IGuidGenerator生成新的Guid值。
无参构造函数
- 务必始终定义一个
protected无参构造函数,以兼容对象关系映射器(ORM)。
引用
- 务必始终通过Id引用其他聚合根。切勿为其他聚合根添加导航属性。
其他类成员
- 务必始终将属性和方法定义为
virtual(显然,私有方法除外)。因为某些ORM和动态代理工具需要此设置。 - 务必保持实体在其自身边界内始终有效且一致。
- 务必在需要保护实体一致性和有效性时,使用
private、protected、internal或protected internalsetter定义属性。 - 务必必要时定义
public、internal或protected internal(虚拟)方法来更改具有非公开setter的属性。 - 务必从setter方法返回实体对象(
this)。
- 务必在需要保护实体一致性和有效性时,使用
聚合根
主键
- 务必始终使用Id属性作为聚合根键。
- 请勿对聚合根使用复合键。
- 务必使用Guid作为所有聚合根的主键。
基类
- 务必根据需求继承
AggregateRoot<TKey>或某个审计类(CreationAuditedAggregateRoot<TKey>、AuditedAggregateRoot<TKey>或FullAuditedAggregateRoot<TKey>)。
聚合边界
- 务必保持聚合尽可能小。大多数聚合仅包含原始属性,不包含子集合。考虑以下设计决策:
- 加载和保存聚合的性能与内存成本(记住聚合通常作为一个单元加载和保存)。较大的聚合将消耗更多CPU和内存。
- 一致性与有效性边界。
示例
聚合根
public class Issue : FullAuditedAggregateRoot<Guid> //使用Guid作为键/标识符
{
public virtual string Title { get; private set; } //通过SetTitle()方法更改
public virtual string Text { get; set; } //可直接更改,允许null值
public virtual Guid? MilestoneId { get; set; } //引用另一个聚合根
public virtual bool IsClosed { get; private set; }
public virtual IssueCloseReason? CloseReason { get; private set; } //仅为枚举类型
public virtual Collection<IssueLabel> Labels { get; protected set; } //子集合
protected Issue()
{
/* 此构造函数供ORM从数据库获取实体时使用。
* - 无需初始化Labels集合,因为将从数据库覆盖。
* - 设为protected,因为代理和反序列化工具可能无法处理私有构造函数。
*/
}
//主构造函数
public Issue(
Guid id, //从调用代码获取Guid值
[NotNull] string title, //指示标题不能为null
string text = null,
Guid? milestoneId = null) //可选参数
{
Id = id;
Title = Check.NotNullOrWhiteSpace(title, nameof(title)); //验证
Text = text;
MilestoneId = milestoneId;
Labels = new Collection<IssueLabel>(); //始终初始化集合
}
public virtual Issue SetTitle([NotNull] string title)
{
Title = Check.NotNullOrWhiteSpace(title, nameof(title)); //验证
return this;
}
/* AddLabel和RemoveLabel方法以安全方式管理Labels集合
*(防止重复添加同一标签) */
public virtual Issue AddLabel(Guid labelId)
{
if (Labels.Any(l => l.LabelId == labelId))
{
return;
}
Labels.Add(new IssueLabel(Id, labelId));
return this;
}
public virtual Issue RemoveLabel(Guid labelId)
{
Labels.RemoveAll(l => l.LabelId == labelId);
return this;
}
/* Close和ReOpen方法保护IsClosed和CloseReason属性的一致性 */
public virtual void Close(IssueCloseReason reason)
{
IsClosed = true;
CloseReason = reason;
}
public virtual void ReOpen()
{
IsClosed = false;
CloseReason = null;
}
}
实体
public class IssueLabel : Entity
{
public virtual Guid IssueId { get; private set; }
public virtual Guid LabelId { get; private set; }
protected IssueLabel()
{
}
public IssueLabel(Guid issueId, Guid labelId)
{
IssueId = issueId;
LabelId = labelId;
}
}
参考文献
- Vaughn Vernon的《有效聚合设计》 http://dddcommunity.org/library/vernon_2011
抠丁客


