Implémenter un Task de A à Z
Bootcode IWA-S04 — Semaine 18, Jour 3
1. Créer les événements
TaskCreated, TaskCompleted, TaskReopened
2. Factory init()
Produire le premier événement à la création
3. Actions
complete() et reopen() avec applyChange
4. Handlers @Handle
Muter l'état pour chaque événement
Rappel du mécanisme
action → applyChange → @Handle → mutation
Les événements
TaskCreated, TaskCompleted, TaskReopened
La factory init()
Créer une Task avec son premier événement
Les actions
complete() et reopen()
Les handlers & test
Vérifier l'état et les uncommitted events
Module 1
Le flux qu'on a vu hier
1. L'action est appelée par le Usecase.
2. L'action crée un événement et appelle applyChange(event).
3. applyChange pousse l'événement dans uncommittedChanges ET appelle le handler.
4. Le handler @Handle mute l'état.
💡 Aujourd'hui on met les mains dans le code : on construit un vrai agrégat.
Module 2
Commencer par les messages
Trois événements métier : création, complétion, réouverture.
// domain/events.ts
export class TaskCreated extends DomainEvent {
constructor(
aggregateId: string,
public readonly title: string
) {
super(aggregateId, { title });
}
}
export class TaskCompleted extends DomainEvent {
constructor(aggregateId: string) {
super(aggregateId, {});
}
}
export class TaskReopened extends DomainEvent {
constructor(aggregateId: string) {
super(aggregateId, {});
}
}
💡 Toujours commencer par les événements : c'est le langage du métier.
Module 3
Créer l'agrégat avec son premier événement
L'état interne de la Task : titre, statut, dates.
// domain/Task.ts
type TaskStatus = 'todo' | 'done';
type TaskProps = {
id: string;
title: string;
status: TaskStatus;
createdAt: Date;
completedAt?: Date;
}
💡 Pas d'ID dans le payload de l'événement : l'aggregateId est déjà dans DomainEvent.
Crée une nouvelle Task et produit le premier événement.
// domain/Task.ts
export class Task extends AggregateRoot<TaskProps> {
static init(id: string, title: string): Task {
const task = new Task({
id,
title,
status: 'todo',
createdAt: new Date(),
});
task.applyChange(new TaskCreated(id, title));
return task;
}
}
💡 init() = création. Elle produit un événement uncommitted.
Module 4
complete() et reopen()
L'action demande un changement d'état via un événement.
// domain/Task.ts
complete(): void {
this.applyChange(
new TaskCompleted(this.props.id)
);
}
reopen(): void {
this.applyChange(
new TaskReopened(this.props.id)
);
}
💡 L'action ne mute pas directement l'état. Elle délègue à applyChange.
Module 5
Muter l'état pour chaque événement
Chaque handler est une pure mutation de l'état. Pas d'effet de bord.
// domain/Task.ts
@Handle(TaskCreated)
onTaskCreated(event: TaskCreated): void {
this.props.title = event.title;
this.props.status = 'todo';
}
@Handle(TaskCompleted)
onTaskCompleted(): void {
this.props.status = 'done';
this.props.completedAt = new Date();
}
@Handle(TaskReopened)
onTaskReopened(): void {
this.props.status = 'todo';
this.props.completedAt = undefined;
}
đź’ˇ Les handlers sont synchrones et sans effet de bord. Ils ne font que mettre Ă jour props.
L'ordre d'implémentation : événements → factory → actions → handlers.
export class Task extends AggregateRoot<TaskProps> {
static init(id: string, title: string): Task {
const task = new Task({ id, title, status: 'todo', createdAt: new Date() });
task.applyChange(new TaskCreated(id, title));
return task;
}
complete(): void { this.applyChange(new TaskCompleted(this.props.id)); }
reopen(): void { this.applyChange(new TaskReopened(this.props.id)); }
@Handle(TaskCreated) onTaskCreated(e: TaskCreated) { this.props.title = e.title; this.props.status = 'todo'; }
@Handle(TaskCompleted) onTaskCompleted() { this.props.status = 'done'; this.props.completedAt = new Date(); }
@Handle(TaskReopened) onTaskReopened() { this.props.status = 'todo'; this.props.completedAt = undefined; }
}
Vérifiez l'état final et les uncommitted events.
// main.ts
const task = Task.init("task-1", "Apprendre l'event sourcing");
task.complete();
task.reopen();
task.complete();
console.log("Status :", task.props.status); // done
console.log("CompletedAt :", task.props.completedAt); // Date
const events = task.getUncommittedEvents();
console.log(events.map(e => e.constructor.name));
// [TaskCreated, TaskCompleted, TaskReopened, TaskCompleted]
✅ L'état est done et il y a 4 événements non commités, dont un TaskReopened.
❌ Muter l'état directement
complete() { this.props.status = 'done'; }
L'événement n'est pas créé, rien n'est persisté.
âś… Passer par applyChange
complete() { this.applyChange(new TaskCompleted(...)); }
L'événement est enregistré ET l'état est muté.
❌ Oublier un handler
Si TaskCompleted n'a pas de @Handle, la Task ne passe jamais Ă done.
✅ Un handler par événement
Chaque événement a sa méthode @Handle dédiée.
❌ Confondre événement et action
L'action est une méthode publique. L'événement est un message immuable qui représente le passé.
✅ Séparation claire
Action = intention. Événement = fait. Handler = mutation.
1. Ordre d'implémentation
Événements → factory init() → actions → handlers.
2. applyChange dans les actions
Chaque action crée un événement et appelle applyChange.
3. @Handle = pure mutation
Les handlers mettent Ă jour props, sans effet de bord.
4. Vérifier les uncommitted events
Le test montre que l'état ET les événements sont cohérents.
Demain : init() vs restore(), et les DomainErrors pour protéger les invariants.