Le pattern Repository

Interface dans le domaine, implémentation dans l'infra

Bootcode IWA-S04 — Semaine 16, Jour 2

Objectifs de la leçon

1. Créer la structure

domain/, infra/, api/

2. Déplacer l'interface

Dans domain/repositories/

3. Déplacer les implémentations

Dans infra/repositories/

4. Vérifier imports & tests

Tout doit toujours fonctionner

Plan du cours

1

Le pattern Repository

Le nom officiel de la séparation interface/implémentation

2

L'interface vit dans domain/

C'est le domaine qui définit ses besoins

3

L'implémentation PostgreSQL dans infra/

Elle implémente le contrat

4

L'implémentation InMemory pour les tests

MĂȘme interface, stockage temporaire

5

Convention de nommage

ArticleRepository vs PostgresArticleRepository

Module 1

Qu'est-ce que le Repository ?

Le nom officiel de ce que vous faites déjà

Le pattern Repository

Un contrat (interface) + des implémentations qui le respectent

📋 L'interface

Définit QUELLES méthodes le domaine a besoin d'appeler

Ex: save(), findById(), delete()

🔧 L'implĂ©mentation

Définit COMMENT ces méthodes fonctionnent réellement

Ex: PostgreSQL, InMemory

💡 Le domaine dit "QUOI", l'infra dit "COMMENT"

Module 2

L'interface dans domain/

Le domaine définit ses besoins

Pourquoi l'interface dans domain/ ?

Parce que c'est le domaine qui DÉFINIT ce dont il a besoin

1ïžâƒŁ Le use case a besoin de sauver un article

→ Il dĂ©finit une mĂ©thode save() dans l'interface

2ïžâƒŁ Le use case a besoin de trouver par ID

→ Il dĂ©finit une mĂ©thode findById() dans l'interface

3ïžâƒŁ L'infra DOIT implĂ©menter ces mĂ©thodes

→ PostgresArticleRepository implĂ©mente l'interface

💡 C'est le Dependency Inversion !

L'infra s'adapte au domaine, pas l'inverse

L'interface en TypeScript

// domain/repositories/ArticleRepository.ts

import { Article } from "../models/Article";

export interface ArticleRepository {

save(article: Article): Promise<void>;

findById(id: string): Promise<Article | null>;

findAll(): Promise<Article[]>;

delete(id: string): Promise<void>;

}

💡 Que des mĂ©thodes, pas d'implĂ©mentation

Aucune référence à TypeORM, PostgreSQL, ou quoi que ce soit de technique

Le use case dépend de l'interface

// domain/usecases/CreateArticle.ts

import { ArticleRepository } from "../repositories/ArticleRepository";

export class CreateArticle {

// Le use case ne connaĂźt QUE l'interface

constructor(private repo: ArticleRepository) {}

async execute(input: { title: string }) {

const article = { id: crypto.randomUUID(), ...input };

await this.repo.save(article); // ← interface

return article;

}

}

💡 Le use case ne sait PAS si c'est PostgreSQL ou InMemory

Il appelle repo.save() et c'est tout — l'implĂ©mentation est injectĂ©e

Module 3

L'implémentation PostgreSQL

Dans infra/ — elle implĂ©mente le contrat

PostgresArticleRepository

// infra/repositories/PostgresArticleRepository.ts

import { DataSource } from "typeorm";

import { ArticleRepository } from "../../domain/repositories/ArticleRepository";

import { ArticleEntity } from "../entities/ArticleEntity";

export class PostgresArticleRepository implements ArticleRepository {

constructor(private db: DataSource) {}

async save(article: Article): Promise<void> {

await this.db.getRepository(ArticleEntity).save(article);

}

async findById(id: string): Promise<Article | null> {

const entity = await this.db.getRepository(ArticleEntity).findOneBy({ id });

return entity ? ArticleMapper.toDomain(entity) : null;

}

}

💡 implements ArticleRepository = respecter le contrat

TypeScript vérifie que toutes les méthodes de l'interface sont présentes

Module 4

L'implémentation InMemory

Pour les tests — mĂȘme interface, stockage temporaire

InMemoryArticleRepository

// infra/repositories/InMemoryArticleRepository.ts

import { ArticleRepository } from "../../domain/repositories/ArticleRepository";

export class InMemoryArticleRepository implements ArticleRepository {

private articles = new Map<string, Article>();

async save(article: Article): Promise<void> {

this.articles.set(article.id, article);

}

async findById(id: string): Promise<Article | null> {

return this.articles.get(id) ?? null;

}

async findAll(): Promise<Article[]> {

return [...this.articles.values()];

}

}

💡 MĂȘme interface, mais avec un Map au lieu de PostgreSQL

Les mĂ©thodes sont async pour respecter l'interface — mĂȘme si c'est instantanĂ©

Deux implémentations, une interface

PostgresArticleRepository

Production

TypeORM + PostgreSQL

←

ArticleRepository

l'interface

→

InMemoryArticleRepository

Tests

Map en mémoire

✅ Le use case ne change JAMAIS

On lui injecte Postgres en prod, InMemory en test — mĂȘme code, mĂȘme interface

Module 5

Convention de nommage

Comment nommer vos fichiers

La convention de nommage

ArticleRepository

L'interface — juste le nom de l'entitĂ© + "Repository"

Dans domain/repositories/

PostgresArticleRepository

L'implĂ©mentation PostgreSQL — prĂ©fixe "Postgres" + nom + "Repository"

Dans infra/repositories/

InMemoryArticleRepository

L'implĂ©mentation InMemory — prĂ©fixe "InMemory" + nom + "Repository"

Dans infra/repositories/

💡 Le prĂ©fixe indique COMMENT, le suffixe indique QUOI

Correspondance avec le codebase Bootcode

Votre projet

Bootcode monorepo

domain/repositories/ArticleRepository.ts

core/repositories/ArticleRepository.ts

infra/repositories/PostgresArticleRepository.ts

adapters/repositories/PostgresArticleRepository.ts

infra/repositories/InMemoryArticleRepository.ts

adapters/repositories/InMemoryArticleRepository.ts

PiÚges courants à éviter

❌ Mettre l'interface dans infra/ au lieu de domain/

✅ L'interface va dans domain/ — le domaine dĂ©finit ses besoins

❌ Ajouter des mĂ©thodes inutiles Ă  l'interface

✅ Seulement ce dont le domaine a besoin (pas de paginate si aucun use case ne pagine)

❌ Oublier les mĂ©thodes async dans le InMemory

✅ Elles doivent respecter la mĂȘme interface — async/await obligatoire

L'interface : que le nécessaire

❌ Trop de mĂ©thodes

interface ArticleRepository {

save()

findById()

findAll()

delete()

paginate() ❌

search() ❌

count() ❌

}

Aucun use case n'utilise paginate, search ou count

✅ Juste ce qu'il faut

interface ArticleRepository {

save()

findById()

findAll()

delete()

}

Seulement les méthodes appelées par les use cases

Points clés à retenir

📌 L'interface est dans domain/ car le domaine DÉFINIT ses besoins

📌 L'infra S'ADAPTE au domaine (Dependency Inversion)

📌 L'interface ne contient QUE ce dont le domaine a besoin

📌 Convention : ArticleRepository vs PostgresArticleRepository

À retenir !

📋

Interface dans domain/

Le domaine définit ses besoins

🔧

Implémentation dans infra/

L'infra s'adapte au contrat

🔄

Deux implémentations

PostgreSQL en prod, InMemory en test

🎯

Que le nécessaire

Pas de méthodes inutiles dans l'interface

Questions ?

Déplacez vos interfaces dans domain/ et vos implémentations dans infra/

Demain : Le pattern Mapper — traducteur entre BDD et domaine