Implementation core/

Agrégats event-sourcés, value objects, erreurs métier et interfaces de repositories

Bootcode IWA-S04 — Semaine 21, Jour 2

Objectifs du jour

1. Implémenter au moins 2 agrégats event-sourcés fonctionnels

Avec pattern init/restore et méthodes métier

2. Créer au moins 1 value object avec validation

Immutable, validé à la construction, private constructor

3. Définir les erreurs métier

DomainError pour chaque rÚgle violée

4. Écrire au moins 4 use cases avec Usecase<I, O>

Chaque use case orchestre un agrégat et un repository

Plan du cours

1

Rappel : la structure core/

agrégats, value objects, repositories, usecases, errors

2

Tips : l'ordre d'implémentation

ÉvĂ©nements → agrĂ©gat → value objects → use cases

3

Circuler et débloquer

Questions d'architecture, dépendances, invariants

4

Point collectif à mi-journée

Questions communes, retours d'expérience

La structure core/

Le domaine pur — zĂ©ro dĂ©pendance externe

# src/core/

domain/

Book.ts # agrégat

Loan.ts # agrégat

BookId.ts, ISBN.ts # value objects

events/

BookAdded.ts, BookBorrowed.ts

errors/

BookAlreadyBorrowedError.ts

repositories/

BookRepository.ts # interface abstraite

usecases/

BorrowBook.ts, ReturnBook.ts

💡 Aucun import de typeorm, inversify, express dans core/. C'est la rùgle absolue.

L'ordre d'implémentation

Suivez cet ordre — chaque Ă©tape dĂ©bloque la suivante

1ïžâƒŁ

Les événements

Ce qui se passe : BookAdded, BookBorrowed, BookReturned

2ïžâƒŁ

L'agrégat

init/restore + méthodes métier qui produisent les événements

3ïžâƒŁ

Les value objects

BookId, ISBN — avec validation

4ïžâƒŁ

Les erreurs métier

Une DomainError par rÚgle violée

5ïžâƒŁ

Les use cases

Orchestrent agrégat + repository

Définir un événement

Au passé, immutable, contient les données de la transition

import { BookId } from '../domain/BookId';

import { MemberId } from '../domain/MemberId';

export class BookBorrowed {

constructor(

public readonly bookId: BookId,

public readonly memberId: MemberId,

public readonly borrowedAt: Date

) {}

}

💡 L'Ă©vĂ©nement capture l'Ă©tat aprĂšs la transition. Pas besoin de stocker l'Ă©tat avant — on rejoue depuis le dĂ©but.

Le pattern init/restore

Deux factory methods : une pour créer, une pour reconstruire depuis l'historique

export class Book extends AggregateRoot<BookEvent> {

private id: BookId;

private isBorrowed = false;

// 1ïžâƒŁ CrĂ©ation — produit l'Ă©vĂ©nement initial

static init(id: BookId, isbn: ISBN): Book {

const book = new Book();

book.recordEvent(new BookAdded(id, isbn));

return book;

}

// 2ïžâƒŁ Rebuild — rejoue l'historique

static restore(events: BookEvent[]): Book {

const book = new Book();

events.forEach(e => book.apply(e));

return book;

}

}

apply() — muter l'Ă©tat depuis un Ă©vĂ©nement

La mĂ©thode apply est privĂ©e — seul restore() l'appelle

private apply(event: BookEvent): void {

if (event instanceof BookAdded) {

this.id = event.bookId;

this.isbn = event.isbn;

this.isBorrowed = false;

} else if (event instanceof BookBorrowed) {

this.isBorrowed = true;

} else if (event instanceof BookReturned) {

this.isBorrowed = false;

}

}

💡 apply() ne fait que muter l'Ă©tat. Elle ne valide rien, ne lance pas d'erreur — l'Ă©vĂ©nement est un fait acquis.

La méthode métier borrow()

Valide l'invariant, puis produit l'événement

borrow(memberId: MemberId): void {

// 1. Valider l'invariant

if (this.isBorrowed) {

throw new BookAlreadyBorrowedError(this.id);

}

// 2. Produire l'événement

this.recordEvent(

new BookBorrowed(this.id, memberId, new Date())

);

// 3. Appliquer immédiatement

this.isBorrowed = true;

}

⚠ L'ordre compte : valider AVANT de produire l'Ă©vĂ©nement. Sinon on crĂ©e un Ă©vĂ©nement pour une transition illĂ©gale.

Value Object avec validation

Private constructor + factory statique qui valide

export class ISBN {

private constructor(public readonly value: string) {}

static create(raw: string): ISBN {

const cleaned = raw.replace(/-/g, '');

if (!isValidIsbn13(cleaned)) {

throw new InvalidISBNError(raw);

}

return new ISBN(cleaned);

}

}

✅ ISBN.create("978-3-16-148410-0") → instance valide

❌ ISBN.create("abc") → InvalidISBNError

Les erreurs métier (DomainError)

Une classe par rĂšgle violĂ©e — dĂšs le dĂ©but, pas aprĂšs

export class BookAlreadyBorrowedError extends DomainError {

constructor(bookId: BookId) {

super(

`Book ${bookId.value} is already borrowed`,

'BOOK_ALREADY_BORROWED'

);

}

}

✅ Bonnes pratiques

  • Code d'erreur lisible : BOOK_ALREADY_BORROWED
  • Message clair avec le contexte
  • Une erreur par rĂšgle mĂ©tier

❌ À Ă©viter

  • throw new Error("erreur") — trop gĂ©nĂ©rique
  • Une seule classe DomainError pour tout
  • Ajouter les erreurs Ă  la fin du projet

L'interface du repository

Abstraite — l'implĂ©mentation vient demain dans adapters/

export interface BookRepository {

findById(id: BookId): Promise<Book | null>;

save(book: Book): Promise<void>;

findByIsbn(isbn: ISBN): Promise<Book | null>;

}

💡 Le repository ne parle qu'en termes du domaine (Book, BookId). Pas de BookEntity TypeORM ici.

Le use case complet

Orchestre : charge l'agrégat, appelle la méthode métier, sauvegarde

export class BorrowBook implements Usecase<BorrowBookInput, BorrowBookOutput> {

constructor(private bookRepo: BookRepository) {}

async execute(input: BorrowBookInput): Promise<BorrowBookOutput> {

// 1. Charger l'agrégat

const book = await this.bookRepo.findById(input.bookId);

if (!book) throw new BookNotFoundError(input.bookId);

// 2. Appeler la méthode métier (l'agrégat valide)

book.borrow(input.memberId);

// 3. Sauvegarder

await this.bookRepo.save(book);

// 4. Retourner le résultat

return { loanId: ..., dueDate: ... };

}

}

Tester le core/ sans base de données

Un InMemoryRepository suffit — on valide la logique mĂ©tier, pas la persistance

export class InMemoryBookRepository implements BookRepository {

private books = new Map<string, Book>();

async findById(id: BookId) {

return this.books.get(id.value) ?? null;

}

async save(book: Book) {

const events = book.pullEvents();

// Rebuild simple : on stocke l'agrégat reconstruit

const restored = Book.restore(events);

this.books.set(book.id.value, restored);

}

}

💡 Si vos tests passent avec l'InMemoryRepository, ils passeront avec le repository PostgreSQL de demain. Le contrat est le mĂȘme.

PiĂšges courants

❌ Importer des dĂ©pendances infrastructure dans core/

Pas de typeorm, inversify, express. Si vous en avez besoin, vous ĂȘtes dans le mauvais dossier.

⚠ Oublier les DomainErrors

Ajoutez-les dÚs le début. Une rÚgle métier sans erreur = un bug silencieux.

❌ AgrĂ©gats trop gros

Un agrégat = une responsabilité. Si Book gÚre aussi les loans, découpez.

⚠ MĂ©langer apply() et logique mĂ©tier

apply() mute l'état sans validation. La validation se fait dans la méthode métier (ex : borrow()).

À retenir !

✅ core/ ne dĂ©pend de RIEN d'externe — pas de TypeORM, pas d'Inversify ici.

✅ Les repositories sont des interfaces abstraites — l'implĂ©mentation vient demain.

✅ Pattern init/restore : init crĂ©e, restore rejoue l'historique.

✅ On peut tester le core/ sans base de donnĂ©es avec un InMemoryRepository.

✅ Ordre : Ă©vĂ©nements → agrĂ©gat → value objects → erreurs → use cases.

Demain : on implĂ©mente adapters/ (TypeORM, mappers, Build) 🚀