Une classe, une action, une méthode execute()
Bootcode IWA-S04 — Semaine 16, Jour 4
1. Transformer un service
Du monolithique aux use cases individuels
2. Un fichier par use case
Dans domain/usecases/
3. Mettre à jour le controller
Injection des use cases
4. Tester chaque use case
Indépendamment, avec ses dépendances
Du service monolithique au Use Case
Une classe par action
Le pattern execute()
Une seule méthode publique
La structure interne
Constructeur → validation → logique → persistance → retour
Nommer les use cases
Verbe + Nom : CreateArticle, SignIn, ResetPassword
Le controller reçoit les use cases
Par injection de dépendances
Module 1
Une classe par action
// services/ArticleService.ts — AVANT
export class ArticleService {
constructor(private repo: ArticleRepository) {}
async create(data) { /* ... */ }
async delete(id) { /* ... */ }
async publish(id) { /* ... */ }
async list() { /* ... */ }
async findById(id) { /* ... */ }
}
❌ Une classe qui fait TOUT
5 méthodes = 5 responsabilités. Difficile à tester, à nommer, à maintenir.
❌ Avant : 1 service
ArticleService.ts
.create()
.delete()
.publish()
.list()
.findById()
✅ Après : 5 use cases
CreateArticle.ts
DeleteArticle.ts
PublishArticle.ts
ListArticles.ts
GetArticle.ts
💡 Chaque use case a un nom explicite
Le nom du fichier = ce que fait le code. On lit l'intention sans ouvrir le fichier.
Testabilité
Chaque use case se teste isolément avec ses propres dépendances mockées.
Lisibilité
Un fichier = une action. On sait exactement où chercher.
Maintenance
Modifier "publish" ne risque pas de casser "delete".
Responsabilité unique (SRP)
Une classe = une raison de changer. Le principe SOLID en pratique.
Module 2
Une seule méthode publique
Un use case = une seule méthode publique : execute()
// domain/usecases/CreateArticle.ts
export class CreateArticle {
constructor(private repo: ArticleRepository) {}
// La SEULE méthode publique
async execute(command: CreateArticleCommand): Promise<Article> {
// ... logique ...
}
}
💡 "execute" = le point d'entrée unique
Le controller appelle toujours la même méthode : useCase.execute(data)
❌ L'erreur
export class ArticleUseCase {
async create() {}
async delete() {}
async publish() {}
}
C'est juste un service renommé !
✅ La solution
export class CreateArticle {
async execute() {}
}
export class DeleteArticle {
async execute() {}
}
Une classe, une action, execute() !
Module 3
Constructeur → validation → logique → persistance → retour
Dépendances dans le constructeur
Le repository est injecté — pas d'import direct
Validation des entrées
Vérifier le command avant toute logique
Logique métier
Le cœur du use case — règles du domaine
Persistance
Sauvegarder via le repository
Retour
Renvoyer l'objet métier ou un résultat
// domain/usecases/CreateArticle.ts
import { Article } from "../models/Article";
import { ArticleRepository } from "../repositories/ArticleRepository";
export class CreateArticle {
// 1️⃣ Dépendances dans le constructeur
constructor(private repo: ArticleRepository) {}
// La seule méthode publique
async execute(command: CreateArticleCommand): Promise<Article> {
// 2️⃣ Validation
if (!command.title || command.title.length < 3) {
throw new Error("Titre trop court");
}
// 3️⃣ Logique métier
const article: Article = {
id: crypto.randomUUID(),
title: command.title,
content: command.content,
authorId: command.authorId,
createdAt: new Date(),
};
// 4️⃣ Persistance
await this.repo.save(article);
// 5️⃣ Retour
return article;
}
}
// domain/usecases/CreateArticle.ts
export interface CreateArticleCommand {
title: string;
content: string;
authorId: string;
}
💡 Le Command = les données d'entrée
Un objet typé qui décrit ce que le use case attend. Pas de paramètres positionnels ambigus.
Module 4
Verbe + Nom
Le nom du fichier = ce que fait le code
CreateArticle.ts
DeleteArticle.ts
PublishArticle.ts
UpdateArticle.ts
SignIn.ts
SignUp.ts
ResetPassword.ts
RefreshToken.ts
✅ On lit l'intention sans ouvrir le fichier
"CreateArticle" → crée un article. "DeleteArticle" → supprime. Simple.
❌ Mauvais noms
ArticleManager.ts
ArticleHelper.ts
ArticleService.ts
Stuff.ts
Vague — on ne sait pas ce que ça fait
✅ Bons noms
CreateArticle.ts
GetArticleById.ts
ListPublishedArticles.ts
ArchiveArticle.ts
Précis — l'action est dans le nom
Module 5
Reçoit les use cases par injection
❌ Avant : un service
constructor(private
service: ArticleService) {}
post() {
return service.create(...);
}
✅ Après : des use cases
constructor(
private create: CreateArticle,
private delete: DeleteArticle,
) {}
post() {
return create.execute(...);
}
💡 Le controller ne fait que déléguer
Il reçoit la requête, appelle execute(), renvoie la réponse. Aucune logique métier.
// api/controllers/ArticleController.ts
import { CreateArticle } from "../../domain/usecases/CreateArticle";
import { GetArticle } from "../../domain/usecases/GetArticle";
export class ArticleController {
constructor(
private createArticle: CreateArticle,
private getArticle: GetArticle,
) {}
// POST /articles
async create(req, res) {
const article = await this.createArticle.execute(req.body);
res.status(201).json(article);
}
// GET /articles/:id
async get(req, res) {
const article = await this.getArticle.execute(req.params.id);
res.json(article);
}
}
// CreateArticle.test.ts
test("crée un article valide", async () => {
// Arrange : un faux repository (InMemory)
const repo = new InMemoryArticleRepository();
const useCase = new CreateArticle(repo);
// Act
const article = await useCase.execute({
title: "Mon titre",
content: "Contenu",
authorId: "user-1",
});
// Assert
expect(article.title).toBe("Mon titre");
expect(repo.articles).toHaveLength(1);
});
💡 Pas de BDD, pas de Express — juste la logique
Le InMemoryRepository respecte la même interface. Le test est rapide et isolé.
packages/iac/core/write/domain/usecases/
CreateArticle.ts
UpdateArticle.ts
DeleteArticle.ts
PublishArticle.ts
ArchiveArticle.ts
📌 Un fichier par action métier
Chaque use case est petit, focalisé, testable. C'est l'architecture professionnelle.
⚠️ "C'est une nouvelle architecture à apprendre"
Non — c'est juste une formalisation. Vous faisiez déjà des services, maintenant chaque action a sa classe.
⚠️ Confondre "domain" et "domaine métier"
Le domain est juste la logique pure — pas de TypeORM, pas d'Express. Les use cases vivent dans domain/.
⚠️ L'interface du repository va dans domain/
Pas dans infra/. C'est contre-intuitif au début, mais c'est le domaine qui définit ses besoins.
⚠️ Mettre de la logique dans le controller
Le controller ne fait que déléguer à execute(). Toute la logique est dans le use case.
📌 Un service de 5 méthodes → 5 use cases
📌 Le nom du fichier = ce que fait le code
📌 Une seule méthode publique : execute()
📌 Chaque use case est testable indépendamment
🎯
Une classe = une action
Verbe + Nom
⚡
execute() = point d'entrée
La seule méthode publique
🧪
Testable isolément
Avec InMemoryRepository
🔌
Injecté dans le controller
Le controller délègue, point
Transformez vos services en use cases
Demain : Projet API blog — tout assembler