Interface dans le domaine, implémentation dans l'infra
Bootcode IWA-S04 â Semaine 16, Jour 2
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
Le pattern Repository
Le nom officiel de la séparation interface/implémentation
L'interface vit dans domain/
C'est le domaine qui définit ses besoins
L'implémentation PostgreSQL dans infra/
Elle implémente le contrat
L'implémentation InMemory pour les tests
MĂȘme interface, stockage temporaire
Convention de nommage
ArticleRepository vs PostgresArticleRepository
Module 1
Le nom officiel de ce que vous faites dĂ©jĂ
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
Le domaine définit ses besoins
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
// 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
// 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
Dans infra/ â elle implĂ©mente le contrat
// 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
Pour les tests â mĂȘme interface, stockage temporaire
// 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Ă©
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
Comment nommer vos fichiers
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
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
â 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
â 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
đ 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
đ
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
Déplacez vos interfaces dans domain/ et vos implémentations dans infra/
Demain : Le pattern Mapper â traducteur entre BDD et domaine