Architecture avancée : couches et patterns
Ports/adapters, repository, strategy, et le discernement.
Objectifs
À la fin de cette leçon, vous saurez :
- situer architecture en couches, hexagonale et clean ;
- comprendre repository, adapter et leurs variantes utiles ;
- poser la bonne question devant chaque pattern.
🔗 Pour vous rafraîchir la mémoire : les bases TypeScript
Des couches à l'hexagone
EN COUCHES (ce que fait Devmind)
Controller → Service → Repository → Base
(dépendance descendante stricte)
HEXAGONALE / CLEAN
┌──────────────────────┐
HTTP ──→ │ domaine (règles) │ ←── persistance
│ ports = interfaces │
└──────────────────────┘
adapters entrants/sortants se branchent sur des PORTS
L'apport hexagonal/clean : le DOMAINE ne dépend de rien de technique. Les dépendances s'inversent — la base implémente une interface définie par le domaine (votre DIP du chapitre 29 poussé à l'architecture). Bénéfice : règles métier testables sans base ni HTTP ; technologies remplaçables.
Coût : indirection supplémentaire. Pour un CRUD simple, c'est de la sur-ingénierie ; pour un domaine riche (facturation, réservation), c'est rentable. Le choix est proportionnel à la complexité RÉELLE du métier.
Repository pattern
Déjà vôtre depuis le chapitre 22 :
interface TaskRepository {
trouver(id: number): Promise<Task | null>;
creer(dto: CreateTaskDto): Promise<Task>;
}
La couche service connaît l'INTERFACE ; PostgreSQL en est une implémentation interchangeable. Testabilité + liberté de stockage.
Adapter pattern
Traduit une interface attendue vers un outil existant :
class S3AssetStorage implements FileStorage { ... } // adapter S3
class LocalFileStorage implements FileStorage { ... } // adapter disque
C'est littéralement le module assets de Devmind : FileStorage est le port, les deux classes sont des adapters. Changer de stockage = changer d'adapter, zéro impact métier.
Strategy, factory, observer — quand ?
| Pattern | Problème résolu | Signal d'usage légitime |
|---|---|---|
| Strategy | comportement choisi à l'exécution | plusieurs algorithmes réellement interchangeables |
| Factory | construction complexe centralisée | création répétée avec variations |
| Observer | notifier N abonnés d'un événement | découplage émetteur/consommateurs réel |
Trois patterns illustrés en cinq lignes chacun
Le tableau précédent dit QUAND ; voyons CE QUE C'EST, en une esquisse chacun :
Strategy — encapsuler des variantes interchangeables d'une même action :
interface FraisLivraison { calculer(poidsKg: number): number; }
class Standard implements FraisLivraison { calculer(kg) { return kg * 2; } }
class Express implements FraisLivraison { calculer(kg) { return kg * 5 + 10; } }
class Commande {
constructor(private frais: FraisLivraison) {}
coutLivraison(kg: number) { return this.frais.calculer(kg); }
}
Signal d'usage : un if/switch sur le « mode » qui risque de grandir à chaque nouveau cas. Vous en avez fait un au chapitre 29 (calculateur de frais, OCP).
Factory — déléguer la création quand elle devient complexe ou dépendante de la configuration :
function creerStockage(config: Config): FileStorage {
if (config.env === "production") return new S3AssetStorage(config.s3);
return new LocalFileStorage(config.uploadDir);
}
Signal d'usage : new répété avec des choix selon l'environnement, dispersés dans le code. Devmind fait exactement cela pour son stockage de fichiers.
Observer — notifier N abonnés sans les connaître :
bus.on("commandePayee", (cmd) => envoyerEmail(cmd));
bus.on("commandePayee", (cmd) => statistiques(cmd));
emettre("commandePayee", commande); // les deux réagissent, sans se connaître
Signal d'usage : une action qui déclenche plusieurs conséquences indépendantes — et où ajouter une conséquence ne doit pas modifier le code de l'action.
Le DDD en une page (pour savoir de quoi on parle)
Le domain-driven design (DDD) part d'un constat : la complexité réelle des applications vit dans le métier (le domaine), pas dans la technique. Ses idées repères, sans en faire la religion :
- un langage ubiquitaire : le code emploie les mots du métier (une leçon se
publie, une proposition estacceptée— pasupdateStatus(3)) ; - des entités définies par leur identité (une leçon garde son identité à travers ses révisions) et des agrégats qui fixent les frontières de cohérence (une révision appartient à SA leçon ; on ne la modifie jamais hors de ces règles) ;
- des services de domaine pour les opérations qui n'appartiennent à aucune entité seule.
Ne pas sur-interpréter : DDD complet vise des domaines riches et complexes. Pour Devmind-scale, retenez surtout la discipline du langage métier dans le code et des frontières de cohérence explicites — vous en faites déjà une partie sans le nommer.
La seule question qui compte
Quel problème ce pattern résout-il ICI, MAINTENANT ?
Si la réponse contient « peut-être qu'un jour », « c'est plus propre », « j'ai lu que » : pas maintenant. Un pattern introduit sans douleur réelle ajoute du code à lire pour personne. Votre historique Git permet de refactorer VERS le pattern le jour où il devient évident — c'est le chemin professionnel normal.
Exercice
- Identifiez ports/adapters dans Devmind (FileStorage, DatabaseService...).
- Dessinez votre todo en version hexagonale : quelles interfaces sortent du domaine ?
- Un collègue veut ajouter Redis cache « au cas où » : argumentez.
Résumé
- Couches = simplicité ; hexagonal = domaine souverain ; choix selon complexité.
- Repository/adapter = vos acquis DIP formalisés.
- Pattern = réponse à une douleur constatée, jamais une anticipation.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Question 3. YAGNI tant qu'aucune mesure n'existe (latence ? charge ?). Le cache ajoute invalidation, cohérence, panne possible — trois problèmes nouveaux. Mesurer d'abord (chapitre monitoring), décider ensuite.