As I showed in a previous post, Anemic Model causes disastrous effects on your project.
My intention in this post is to use the example of Vaughn Vernon’s book [IDDD, 2013] on the scenario of a SCRUM model and be able to show in a practical way the difference between an implementation of Anemic Model and Rich Model.
Get to work!!
Let’s say that the Product Owner:
Allow each Backlog Item to be allocated to a Sprint. If it has already been allocated to a different Sprint, it must first be deallocated. After the allocation is complete, notify the interested parties.
A very trivial scenario, which can be shown hierarchically below:
Despite the representation in the drawing we will ignore Task in our example.
Anemic Model
Domain/Entities
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; }
}
Domain/Services
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);
}
}
Can you see the problems of this model?
Note that their entities have no business logic, every rule was directed to Domain Services, entity properties are set without any control, there is no validation after setting these properties, the aggregates do not generate domain events.
Your objects are data containers.
Now a Rich Model …
Domain/Entities
public class Sprint : Entity
{
public Sprint(string description, DateTime beginDate, DateTime endDate)
{
Status = SprintStatus.New;
Description = description;
BeginDate = beginDate;
EndDate = endDate;
Validate();
}
public int Id { get; private set; }
public IList<BacklogItem> BacklogItems { get; private set; }
public SprintStatus Status { get; private set; }
public string Description { get; private set; }
public DateTime BeginDate { get; private set; }
public DateTime EndDate { get; private set; }
public void SetStatusToNew() => Status = SprintStatus.New;
public void SetStatusToInExecution() => Status = SprintStatus.InExecution;
public void SetStatusToClosed() => Status = SprintStatus.Closed;
public void SetDescription(string description) => Description = description;
public void SetBeginDate(DateTime beginDate) => BeginDate = beginDate;
public void SetEndDate(DateTime endDate) => EndDate = 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 = BacklogItemStatus.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 BacklogItemStatus 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 = BacklogItemStatus.New;
public void SetStatusToCommitted() => Status = BacklogItemStatus.Committed;
public void SetStatusToApproved() => Status = BacklogItemStatus.Approved;
public void SetStatusToDone() => Status = BacklogItemStatus.Done;
public void SetDescription(string description) => Description = description;
public void SetBeginDate(DateTime? beginDate) => BeginDate = beginDate;
public void SetEndDate(DateTime? endDate) => EndDate = 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...
}
}
Application
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);
}
}
Can you see the difference?
The first example uses a very data-centric approach, not behavioral. It’s not really a domain model.
In our Rich Model example we use the behavior of the domain object that expresses the Ubiquitous Language.
Instead of exposing data attributes to clients, it exposes a behavior that explicitly and clearly indicates that a Client can allocate a Backlog Item to a Sprint.
Without inserting this rich behavior into the Backlog Item, the Client would have to work with events, and this is extremely wrong.
In the second example the benefits are much greater.
And now, can you see the benefits of working with a Rich Model?
That’s all, folks
Source Code: https://github.com/felipefbatista/ddd_anemic_rich_model
This made my day! finally got this. Thanks for your post!
Great post, very enlightening.
I just had a question about the code snippet that sends the E-mail. Is it still the responsibility of the Domain Service? I didn’t find the representation of this code snippet in the Rich Model.
Congratulations on the article!
It is not the responsibility of the domain service to send email.
The recommendation is that you issue a notification domain event and have an infrastructure layer to send that email.
Very Helpful Thank You much