DDD serialized article
What does Eric Evans say about the definition of DDD In this article I wrote that the definition of Domain Driven Development looks like this:
- Focus on the core complexity and opportunities of your domain
So how is the verbalized model of domain knowledge ultimately represented in code?
First, when we think about business constraints, we think about *** invariants ***.
Invariant: A condition that must always be consistent for the life of the model.
There are various names such as specifications and constraints, but the term invariant is used as the term for DDD, so we will unify it.
For example, suppose you want to model a task in a business application. After defining the requirements, I found that the following invariant conditions must be met.
Let's implement this.
@Entity
public class Task {
@Id
@GeneratedValue
private Long id;
private TaskStatus taskStatus;
private String name;
private LocalDate dueDate;
private int postponeCount;
public static final int POSTPONE_MAX_COUNT = 3;
public Task() {
}
/*
*Setter for all items
*/
public void setId(Long id) { this.id = id; }
public void setTaskStatus(TaskStatus taskStatus) { this.taskStatus = taskStatus; }
public void setName(String name) { this.name = name; }
public void setDueDate(LocalDate dueDate) { this.dueDate = dueDate; }
public void setPostponeCount(int postponeCount) { this.postponeCount = postponeCount; }
/*
*Getter for all items
*/
public Long getId() { return this.id; }
public String getName() { return this.name; }
public TaskStatus getTaskStatus() { return this.taskStatus; }
public LocalDate getDueDate() { return this.dueDate; }
public int getPostponeCount() { return this.postponeCount; }
/*
*A method that barely has behavior
*/
public boolean canPostpone() { return this.postponeCount < POSTPONE_MAX_COUNT; }
}
public enum TaskStatus {
UNDONE, DONE
}
public class TaskApplication {
@Autowired
private TaskRepository taskRepository;
public void createTask(String name, LocalDate dueDate) {
if (name == null || dueDate == null) {
throw new IllegalArgumentException("Required items are not set");
}
Task task = new Task();
task.setTaskStatus(TaskStatus.UNDONE);
task.setName(name);
task.setDueDate(dueDate);
task.setPostponeCount(0);
taskRepository.save(task);
}
public void postponeTask(Long taskId) {
Task task = taskRepository.findById(taskId);
if (!taks.canPostpone()) {
throw new IllegalArgumentException("The maximum number of postponements has been exceeded");
}
task.setDueDate(task.getDueDate().plusDays(1L));
task.setPostponeCount(task.getPostponeCount() + 1);
taskRepository.save(task);
}
//Completion processing is omitted
}
It's done! It looks like it meets the requirements. As far as the TaskApplication class is seen, there seems to be no problem in creating and updating Tasks. Let's release it now.
However, a few weeks after the release, an engineer who was not familiar with the business implemented the following code.
public class AnotherTaskApplication {
@Autowired
private TaskRepository taskRepository;
public void createDoneTask(String name, LocalDate dueDate) {
Task task = new Task();
task.setTaskStatus(TaskStatus.DONE); //× Task generation in completed state
task.setPostponeCount(-1); //× The count is a minus
taskRepository.save(task);
}
public void changeTask(Long taskId, String name, LocalDate dueDate, TaskStatus taskStatus) {
Task task = taskRepository.findById(taskId);
task.setName(name); //× Change the task name that should not be changed
task.setDueDate(dueDate); //× Set the due date arbitrarily with the input value, ignore the number of postponements
task.setTaskStatu(taskStatus); //× Tasks can be returned to incomplete
taskRepository.save(task); }
}
It's amazing, I was able to destroy the invariants.
The trouble is that the AnotherTaskAPplication class is created and implemented, so the person who implemented the original TaskApplication class did not notice that such processing was written in another class. (Let's leave the review system for now)
In this example, the specification is expressed by Application, but the Task model itself, which has Setter / Getter in all items, cannot express anything.
This condition is called *** domain model anemia ***.
This object, which cannot represent invariants, is no longer a model, but just a data model *** that projects a relational model onto the object. And such an application can be called *** transaction script ***.
So how can we get the model to express invariants?
@Entity
class Task {
@Id
@GeneratedValue
private Long id;
private TaskStatus taskStatus;
private String name;
private LocalDate dueDate;
private int postponeCount;
private static final int POSTPONE_MAX_COUNT = 3;
/*
*Constructor: Represents an invariant when creating an entity
*/
public Task(String name, LocalDate dueDate) {
if (name == null || dueDate == null) {
throw new IllegalArgumentException("Required items are not set");
}
this.name = name;
this.dueDate = dueDate;
this.taskStatus = TaskStatus.UNDONE;
this.postponeCount = 0;
}
/*
*State transition method: Represents an invariant related to the state transition of a created entity
*/
public void postpone() {
if (postponeCount >= POSTPONE_MAX_COUNT) {
throw new IllegalArgumentException("The maximum number of postponements has been exceeded");
}
dueDate.plusDays(1L);
postponeCount++;
}
public void done() {
this.taskStatus = TaskStatus.DONE;
}
//The name setter doesn't exist, so you can't change the name
/*
*getter, state acquisition method
*/
public Long getId() { return id; }
public String getName() { return name; }
public LocalDate getDueDate() { return dueDate; }
public boolean isUndone() { return this.taskStatus == TaskStatus.UNDONE; }
public boolean canPostpone() { return this.postponeCount < POSTPONE_MAX_COUNT; }
}
//In the same package, enum is defined as package private
enum TaskStatus {
UNDONE, DONE
}
public class TaskApplication {
@Autowired
private TaskRepository taskRepository;
public void createTask(String name, LocalDate dueDate) {
Task task = new Task(name, dueDate); //The task instance is always created in a form that satisfies the invariant condition.
taskRepository.save(task);
}
public void postpone(Long taskId) {
Task task = taskRepository.findById(taskId);
task.postpone(); //Processing that destroys invariants cannot be written from application for the acquired object.
taskRepository.save(task);
}
//Completion processing is omitted
//Validation is omitted
}
When the process of imposing invariants was transferred to the model ... A very expressive model was created! !!
The invariant condition is beautifully expressed by the constructor of the Task model and the state transition method.
Some of the benefits of this design include:
It's a good thing.
If you write this, you can say that *** the model expresses the knowledge of the domain ***.
Let's look back at the definition of Domain Driven.
- Focus on the core complexity and opportunities of your domain
In the above example, I was able to write software that represented the model.
This design alone has had great benefits, but Domain Driven doesn't end there.
What kind of behavior does such a model behave, and how will it change when the specifications are changed? By matching the definition of words with business (domain) experts, we always pursue the latest and more expressive model and follow the code.
This is literally *** domain model driven development ***.
I hope you read this article and think, "Oh, maybe DDD?" I will continue to write articles to spread DDD in the future, so please keep in touch with me if you like.
Recommended Posts