在这篇文章中使用Vaughn Vernon的书[ IDDD,2013 ] 的例子来描述SCRUM模型的情景,并能够以实际的方式展示贫血模型和富模型的实现之间的区别。
让我们说产品负责人:
允许将每个积压项分配给Sprint。如果它已经分配给不同的Sprint,则必须先将其解除分配。分配完成后,通知相关方。
一个非常简单的场景,可以在下面分层显示:

尽管在图中有表示,但我们将在示例中忽略Task。
贫血模型
领域/实体
public class Sprint { public int Id { get; set; } public IList<BacklogItem> BacklogItems { get; set; } public SprintStatusEnum Status { get; set; } public string Description { get; set; } public DateTime BeginDate { get; set; } public DateTime EndDate { get; set; } }
public class BacklogItem { public int Id { get; set; } public IList<Task> Tasks { get; set; } public int? SprintId { get; set; } public int? UserId { get; set; } public BacklogItemStatusEnum Status { get; set; } public string Description { get; set; } public DateTime? BeginDate { get; set; } public DateTime? EndDate { get; set; } }
|
领域/服务
public class SprintServices : ISprintServices { private readonly ISprintRepository _sprintRepository; private readonly IBacklogItemRepository _backlogItemRepository; public SprintServices(ISprintRepository sprintRepository, IBacklogItemRepository backlogItemRepository) { _sprintRepository = sprintRepository; _backlogItemRepository = backlogItemRepository; }
public void InsertBacklogItem(int sprintId, int backLogItemId) { var sprint = _sprintRepository.GetById(sprintId); var backLogItem = _backlogItemRepository.GetById(backLogItemId);
backLogItem.SprintId = sprintId; backLogItem.Status = BacklogItemStatusEnum.Committed;
EmailService.SendMail("destination@email.com", $"The backlog item '{backLogItem.Description}' was assigned to Sprint '{sprint.Description}'");
_backlogItemRepository.Update(backLogItem); } }
|
请注意,它们的实体没有业务逻辑,每个规则都依赖领域服务,实体属性设置没有任何控制,设置这些属性后没有验证,聚合不生成域事件。
这样的对象只是数据容器。
现在看看富血模型......
域/实体
public class Sprint : Entity { public Sprint(string description, DateTime beginDate, DateTime endDate) { Status = SprintStatusEnum.New; Description = description; BeginDate = beginDate; EndDate = endDate;
Validate(); }
public int Id { get; private set; } public IList<BacklogItem> BacklogItems { get; private set; } public SprintStatusEnum Status { get; private set; } public string Description { get; private set; } public DateTime BeginDate { get; private set; } public DateTime EndDate { get; private set; }
public void SetStatus(SprintStatusEnum status) => Status = status; public void SetDescription(string description) => Description = description; public void SetBeginDate(DateTime beginDate) => BeginDate = beginDate; public void SetEndDate(DateTime endDate) => BeginDate = endDate;
public void Validate() { if (string.IsNullOrEmpty(Description)) { throw new Exception("Description can not be null"); }
if (BeginDate > EndDate) { throw new Exception("EndDate must be greater than BeginDate"); }
//more rules... } } public class BacklogItem : Entity { public BacklogItem(string description) { Status = BacklogItemStatusEnum.New; Description = description;
Validate(); }
public int Id { get; private set; } public IList<Task> Tasks { get; private set; } public int? SprintId { get; private set; } public int? UserId { get; private set; } public BacklogItemStatusEnum Status { get; private set; } public string Description { get; private set; } public DateTime? BeginDate { get; private set; } public DateTime? EndDate { get; private set; }
public void SetSprintId(int? sprintId) => SprintId = sprintId; public void SetUserId(int? userId) => UserId = userId; public void SetStatusToNew() => Status = BacklogItemStatusEnum.New; public void SetStatusToCommitted() => Status = BacklogItemStatusEnum.Committed; public void SetStatusToApproved() => Status = BacklogItemStatusEnum.Approved; public void SetStatusToDone() => Status = BacklogItemStatusEnum.Done; public void SetDescription(string description) => Description = description; public void SetBeginDate(DateTime? beginDate) => BeginDate = beginDate; public void SetEndDate(DateTime? endDate) => BeginDate = endDate;
public void CommitToSprint(Sprint sprint) { if (IsCommittedToSprint()) { UncommitFromSprint(); }
SetStatusToCommitted(); SetSprintId(sprint.Id);
this.AddDomainEvent(new BacklogItemCommitted { Id = Id, SprintId = SprintId.Value }); } public void UncommitFromSprint() { SprintId = null;
this.AddDomainEvent(new BacklogItemUncommitFromSprint { Id = Id, SprintId = SprintId.Value }); } public bool IsCommittedToSprint() => SprintId != null && SprintId != default(int);
public void Validate() { if (string.IsNullOrEmpty(Description)) { throw new Exception("Description can not be null"); }
//more rules... } }
|
应用:
public class BoardApplication : IBoardApplication { private readonly ISprintRepository _sprintRepository; private readonly IBacklogItemRepository _backlogItemRepository; public BoardApplication(ISprintRepository sprintRepository, IBacklogItemRepository backlogItemRepository) { _sprintRepository = sprintRepository; _backlogItemRepository = backlogItemRepository; }
public void ToAllocateBacklogItemToaSprint(int sprintId, int backLogItemId) { var sprint = _sprintRepository.GetById(sprintId); var backLogItem = _backlogItemRepository.GetById(backLogItemId);
backLogItem.CommitToSprint(sprint);
_backlogItemRepository.Update(backLogItem); } }
|
你能看到区别么?
第一个例子使用了一种非常以数据为中心的方法,而不是行为方法。它不是一个真正的领域模型。
在我们的Rich Model示例中,我们使用表达泛在语言的域对象的行为。
它不会向客户端公开数据属性,而是公开一种行为,该行为明确且清楚地表明客户可以将Backlog项目分配给Sprint。
如果不将此丰富的行为插入Backlog项,则客户端必须处理事件,这是非常错误的。
在第二个例子中,好处要大得多。
现在,您能看到使用Rich Model的好处吗?