贫血模型与充血模型比较 - DDD - The Domain Driven Design


在这篇文章中使用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的好处吗?