Le Use Case

Une classe, une action, une méthode execute()

Bootcode IWA-S04 — Semaine 16, Jour 4

Objectifs de la leçon

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

Plan du cours

1

Du service monolithique au Use Case

Une classe par action

2

Le pattern execute()

Une seule méthode publique

3

La structure interne

Constructeur → validation → logique → persistance → retour

4

Nommer les use cases

Verbe + Nom : CreateArticle, SignIn, ResetPassword

5

Le controller reçoit les use cases

Par injection de dépendances

Module 1

Du service au Use Case

Une classe par action

Le service monolithique : le problème

// 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.

La transformation : 1 service → 5 use cases

❌ 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.

Pourquoi séparer ?

🧪

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

Le pattern execute()

Une seule méthode publique

La règle d'or du Use Case

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)

Une seule méthode publique !

❌ 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

La structure interne

Constructeur → validation → logique → persistance → retour

Anatomie d'un use case

1️⃣

Dépendances dans le constructeur

Le repository est injecté — pas d'import direct

2️⃣

Validation des entrées

Vérifier le command avant toute logique

3️⃣

Logique métier

Le cœur du use case — règles du domaine

4️⃣

Persistance

Sauvegarder via le repository

5️⃣

Retour

Renvoyer l'objet métier ou un résultat

CreateArticle en TypeScript

// 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;

}

}

Le Command : l'entrée typée

// 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

Nommer les use cases

Verbe + Nom

La convention : 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.

Bien nommer ses use cases

❌ 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

Le controller

Reçoit les use cases par injection

Le controller reçoit les use cases

❌ 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.

ArticleController en TypeScript

// 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);

}

}

Tester un use case indépendamment

// 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é.

Dans le codebase Bootcode

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.

Pièges courants

⚠️ "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.

Points clés à retenir

📌 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

À retenir !

🎯

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

Questions ?

Transformez vos services en use cases

Demain : Projet API blog — tout assembler