This guide explains principles that are not dependent on a specific programming language and can be applied in any language. Code examples are shown in TypeScript, but the concepts can be applied to other languages as well.
Test-First Domain Model Design and Implementation
Purpose
Implement pure domain logic that is not affected by external dependencies.
Specific Steps
- Define the behavior of the domain model as tests
- Clarify the functions and constraints the model should have
- Consider edge cases as well
- Implement the domain model to satisfy the tests
- Implement according to the specifications indicated by the tests
- Follow the design principles of value objects, entities, and aggregates
- Refine the design through refactoring
- Eliminate code duplication
- Clarify and separate responsibilities
- Improve naming
Merit
- Clarification of specifications and promotion of understanding of requirements
- Improvement of design quality (separation of responsibilities and increased cohesion)
- Ensuring the safety of refactoring
- Early detection of edge cases
- Value as documentation
Test Examples
// Taskのテスト例
describe("Task", () => {
it("タスクが完了しているかどうかを確認できる", () => {
const taskId = TaskId.of("task-1");
const title = TaskTitle.of("テストタスク");
const task = Task.of({
id: taskId,
title: title,
completed: false,
createdAt: new Date("2023-03-01T10:00:00")
});
expect(task.isCompleted()).toBe(false);
const completedTask = task.markAsCompleted();
expect(completedTask.isCompleted()).toBe(true);
});
it("期限日が未来の日付であることを検証できる", () => {
const pastDate = new Date();
pastDate.setDate(pastDate.getDate() - 1); // 昨日の日付
const result = Task.validateDueDate(pastDate);
expect(result.isFailure()).toBe(true);
expect(result.error.message).toContain("期限は未来の日付である必要があります");
});
});
Implementation of an In-Memory Repository
Purpose
- Enable test execution of domain models and use cases without depending on a database
Specific Steps
- Definition of the repository interface
- Define the operations required by the domain model
- Creation of the in-memory implementation
- Use an in-memory collection
- Simulate the behavior of an actual database
Use Case Development
Purpose
- Implement application-layer logic using domain models and repositories
Specific Steps
- Define tests for the use case
- Clarify execution conditions and expected results
- Cover both success cases and failure cases
- Implement the use case to satisfy the tests
- Use appropriate domain models and repositories
- Apply business rules
- Refactoring
- Separation and clarification of responsibilities
Test Examples
describe("CompleteTaskUseCase", () => {
let taskRepository: TaskRepositoryInMemory;
let completeTaskUseCase: CompleteTaskUseCase;
beforeEach(() => {
taskRepository = new TaskRepositoryInMemory();
const task1 = Task.of({
id: TaskId.of("task-1"),
title: TaskTitle.of("未完了タスク"),
completed: false,
createdAt: new Date("2023-03-01T10:00:00"),
});
const task2 = Task.of({
id: TaskId.of("task-2"),
title: TaskTitle.of("既に完了しているタスク"),
completed: true,
createdAt: new Date("2023-03-01T13:00:00"),
});
taskRepository.save(task1);
taskRepository.save(task2);
completeTaskUseCase = new CompleteTaskUseCase(taskRepository);
});
it("タスクを完了できる", async () => {
const result = await completeTaskUseCase.execute({
taskId: "task-1",
});
expect(result.isSuccess()).toBe(true);
const updatedTask = await taskRepository.findById(TaskId.of("task-1"));
expect(updatedTask.isSuccess()).toBe(true);
expect(updatedTask.value.isCompleted()).toBe(true);
});
it("存在しないタスクを処理できる", async () => {
const result = await completeTaskUseCase.execute({
taskId: "non-existent",
});
expect(result.isFailure()).toBe(true);
expect(result.error.message).toContain("タスクが見つかりません");
});
});
Implementation of Interface Adapters and Infrastructure
Purpose
- Connect domain models and use cases to actual infrastructure
Specific Steps
-
Implementation of the persistence repository
- Connection to the actual database
- Implementation of the repository interface
-
Implementation of the controller
- Validation of input and bridging to the use case
-
Implementation of the presentation layer
- Conversion of use case results into a display format
Integration testing and combined testing
Purpose
- Verify the behavior of the entire system
Specific steps
-
Creation of integration tests
- Testing using actual infrastructure
- End-to-end behavior verification
-
Execution of combined tests
- Verify collaboration between components
Risks of not following this development procedure
In design and implementation centered on the domain model, if the order shown in this knowledge (domain model → in-memory repository → use case → infrastructure) is not followed, the following serious risks arise:
-
Distortion of domain model design
- The domain model design is inappropriately affected by infrastructure or UI constraints
- Entity design becomes influenced by the database schema
- Compromise the domain model to match the constraints of the O/R mapper framework
-
Leakage of technical concerns
- Technical concerns such as persistence and transaction management become mixed into domain logic
- The domain model becomes framework-dependent and deviates from pure business logic
-
Increased difficulty of testing
- Models containing many external dependencies make testing complex and reduce coverage
- Slower test execution speed slows down the feedback cycle
-
Reduced ease of change
- Dependencies with external layers become complex, expanding the scope of impact of changes
- Situations arise in which changes to the domain model force changes to the infrastructure
-
Loss of conceptual consistency
- Technical terms become mixed into the model instead of the domain experts' language (ubiquitous language)
- The expression of business rules becomes unclear due to technical constraints
Summary: Advantages of this development approach
-
The domain model design is not distorted
- Pure domain design that is not dragged down by infrastructure constraints
- Clear separation of business rules and technical concerns
-
Early detection and easy correction of bugs
- Tests clarify specifications and quickly detect the impact of changes
- Unit-level test coverage increases
-
Flexible response to requirement changes
- A robust test foundation enables safe refactoring
- Changes to domain logic do not depend on infrastructure
-
Value as documentation
- Tests document domain rules and behavior
- Consistent use of ubiquitous language is promoted
Related skills (recommended for concurrent reading)
When using this skill, also refer to the following skills:
domain-building-blocks: Design of building blocks subject to TDD (value objects, entities, etc.)aggregate-design: Aggregate design rules and boundary determinationrepository-design: Repository design patterns, including in-memory repositories