17 KiB
ADR-005 : Patterns de Conception Appliqués
Statut : ✅ Accepté
Date : Octobre 2025
Décideurs : Équipe Architecture
Tags : patterns, design-patterns, clean-code
Contexte
Pour garantir la maintenabilité, la testabilité, et le découplage du système, nous devons appliquer des Design Patterns éprouvés.
L'objectif est de démontrer la maîtrise de patterns architecturaux modernes (au-delà de la simple implémentation fonctionnelle).
Besoins identifiés
- Découplage : Indépendance entre couches
- Testabilité : Code facilement testable unitairement
- Réutilisabilité : Éviter duplication
- Maintenabilité : Code compréhensible et évolutif
Décision
✅ Nous appliquons 5 patterns principaux :
- Repository Pattern (Accès aux données)
- Service Layer Pattern (Logique métier)
- Dependency Injection (DI) (Découplage)
- Factory Pattern (Création d'objets)
- Observer Pattern via Event Bus (Communication asynchrone)
Patterns Détaillés
1️⃣ Repository Pattern
Objectif : Abstraire l'accès aux données, découpler le domaine de l'infrastructure.
Principe
Domain Layer → Interface Repository
Infrastructure Layer → Implementation (Supabase)
Le domaine ne connaît PAS Supabase ✅
Exemple : Reservation Service
Interface (Domain Layer)
// packages/microservices/reservation/src/domain/repositories/IReservationRepository.ts
export interface IReservationRepository {
findById(id: string): Promise<Reservation | null>
findByUserId(userId: string): Promise<Reservation[]>
findByOutilId(outilId: string): Promise<Reservation[]>
create(data: CreateReservationDTO): Promise<Reservation>
update(id: string, data: UpdateReservationDTO): Promise<Reservation>
delete(id: string): Promise<void>
findOverlapping(params: OverlapParams): Promise<Reservation[]>
}
Implémentation (Infrastructure Layer)
// packages/microservices/reservation/src/infrastructure/repositories/SupabaseReservationRepository.ts
import { SupabaseClient } from '@supabase/supabase-js'
import { IReservationRepository } from '@/domain/repositories/IReservationRepository'
export class SupabaseReservationRepository implements IReservationRepository {
constructor(private supabase: SupabaseClient) {}
async findById(id: string): Promise<Reservation | null> {
const { data, error } = await this.supabase
.from('reservations')
.select('*')
.eq('id', id)
.single()
if (error) throw new RepositoryError(error.message)
return data ? ReservationMapper.toDomain(data) : null
}
// ... autres méthodes
}
Avantages
✅ Testabilité : Mock facile du repository
// Test
const mockRepo: IReservationRepository = {
findById: jest.fn().mockResolvedValue(mockReservation),
// ...
}
✅ Changement de DB : Remplacer Supabase par Prisma/MongoDB sans toucher au domaine
✅ Séparation des responsabilités : Le domaine ne gère pas les requêtes SQL
2️⃣ Service Layer Pattern
Objectif : Centraliser la logique métier, orchestrer les use cases.
Principe
API Routes → Services → Repositories
Les services contiennent la logique métier complexe et orchestrent les appels.
Exemple : Reservation Service
Service Métier
// packages/microservices/reservation/src/application/services/ReservationService.ts
export class ReservationService {
constructor(
private reservationRepo: IReservationRepository,
private inventoryService: InventoryService, // Dépendance autre microservice
private notificationService: NotificationService,
private eventBus: EventBus
) {}
async createReservation(
userId: string,
data: CreateReservationDTO
): Promise<Reservation> {
// 1. Valider les données
this.validateReservationDates(data)
// 2. Vérifier disponibilité (appel Inventory Service)
const isAvailable = await this.inventoryService.checkAvailability({
outilId: data.outilId,
entrepotId: data.entrepotId,
dateDebut: data.dateDebut,
dateFin: data.dateFin
})
if (!isAvailable) {
throw new BusinessError('OUTIL_NON_DISPONIBLE')
}
// 3. Créer la réservation
const reservation = await this.reservationRepo.create({
...data,
userId,
status: 'PENDING'
})
// 4. Émettre un event (asynchrone)
await this.eventBus.publish('reservation.created', {
reservationId: reservation.id,
outilId: data.outilId,
entrepotId: data.entrepotId,
userId
})
// 5. Envoyer notification
await this.notificationService.sendReservationConfirmation(userId, reservation)
return reservation
}
private validateReservationDates(data: CreateReservationDTO): void {
if (data.dateDebut >= data.dateFin) {
throw new ValidationError('Date de fin doit être après date de début')
}
if (data.dateDebut < new Date()) {
throw new ValidationError('Date de début ne peut pas être dans le passé')
}
}
}
Avantages
✅ Logique métier centralisée : Pas dispersée dans les API routes
✅ Réutilisabilité : Le service peut être appelé depuis n'importe où
✅ Testabilité : Tester la logique sans API HTTP
✅ Orchestration : Coordonne plusieurs repositories et services
3️⃣ Dependency Injection (DI)
Objectif : Inverser les dépendances, faciliter les tests et le découplage.
Principe (IoC - Inversion of Control)
Avant (Bad) ❌
class ReservationService {
private repo = new SupabaseReservationRepository() // Hard-coded !
}
Après (Good) ✅
class ReservationService {
constructor(private repo: IReservationRepository) {} // Interface !
}
Implémentation avec Container
Container DI
// packages/shared/di/src/container.ts
import { Container } from 'inversify'
const container = new Container()
// Repositories
container.bind<IReservationRepository>('IReservationRepository')
.to(SupabaseReservationRepository)
.inSingletonScope()
// Services
container.bind<ReservationService>('ReservationService')
.to(ReservationService)
.inSingletonScope()
export { container }
Injection dans API Route
// apps/moderne/app/api/reservations/route.ts
import { container } from '@bricoloc/shared-di'
import { ReservationService } from '@bricoloc/reservation-service'
export async function POST(req: Request) {
const service = container.get<ReservationService>('ReservationService')
const data = await req.json()
const reservation = await service.createReservation(userId, data)
return Response.json(reservation)
}
Avantages
✅ Testabilité : Injecter des mocks facilement
// Test
container.bind<IReservationRepository>('IReservationRepository')
.toConstantValue(mockRepository)
✅ Découplage : Le service ne connaît que l'interface
✅ Configuration centralisée : Toutes les bindings au même endroit
✅ Lifetime management : Singleton, Transient, Scoped
4️⃣ Factory Pattern
Objectif : Centraliser la création d'objets complexes.
Exemple : Entity Factory
Use Case : Créer une entité Reservation avec validation et business rules
Factory
// packages/microservices/reservation/src/domain/factories/ReservationFactory.ts
export class ReservationFactory {
static create(data: CreateReservationDTO, userId: string): Reservation {
// 1. Validation
this.validateData(data)
// 2. Calculer le prix
const prix = this.calculatePrice(data.outilId, data.dateDebut, data.dateFin)
// 3. Déterminer le status initial
const status = this.determineInitialStatus(data)
// 4. Créer l'entité
return new Reservation({
id: generateId(),
userId,
outilId: data.outilId,
entrepotId: data.entrepotId,
dateDebut: data.dateDebut,
dateFin: data.dateFin,
prix,
status,
createdAt: new Date()
})
}
private static validateData(data: CreateReservationDTO): void {
if (!data.outilId) throw new ValidationError('outilId requis')
if (!data.dateDebut) throw new ValidationError('dateDebut requis')
// ...
}
private static calculatePrice(
outilId: string,
dateDebut: Date,
dateFin: Date
): number {
// Logique de pricing
const days = differenceInDays(dateFin, dateDebut)
const dailyRate = 15 // €/jour
return days * dailyRate
}
private static determineInitialStatus(data: CreateReservationDTO): string {
// Si réservation immédiate → PENDING
// Si réservation future → SCHEDULED
return data.dateDebut <= new Date() ? 'PENDING' : 'SCHEDULED'
}
}
Usage dans Service
async createReservation(userId: string, data: CreateReservationDTO) {
// Factory s'occupe de toute la complexité
const reservation = ReservationFactory.create(data, userId)
return await this.reservationRepo.create(reservation)
}
Avantages
✅ Encapsulation : Logique de création centralisée
✅ Validation : Garantit que l'objet est valide
✅ Business Rules : Applique les règles métier (pricing, status)
✅ Testabilité : Tester la création isolément
5️⃣ Observer Pattern via Event Bus
Objectif : Communication asynchrone entre microservices.
Principe
Service A → Émet Event → Event Bus → Service B écoute
Découplage : Service A ne connaît PAS Service B ✅
Implémentation avec Supabase Realtime
Event Bus Abstraction
// packages/shared/events/src/EventBus.ts
export interface IEventBus {
publish(eventName: string, data: any): Promise<void>
subscribe(eventName: string, handler: EventHandler): void
unsubscribe(eventName: string, handler: EventHandler): void
}
export class SupabaseEventBus implements IEventBus {
constructor(private supabase: SupabaseClient) {}
async publish(eventName: string, data: any): Promise<void> {
const channel = this.supabase.channel('events')
await channel.send({
type: 'broadcast',
event: eventName,
payload: data
})
}
subscribe(eventName: string, handler: EventHandler): void {
const channel = this.supabase.channel('events')
channel.on('broadcast', { event: eventName }, handler)
channel.subscribe()
}
// ...
}
Publisher (Reservation Service)
async createReservation(...) {
const reservation = await this.reservationRepo.create(data)
// Émet un event
await this.eventBus.publish('reservation.confirmed', {
reservationId: reservation.id,
outilId: data.outilId,
entrepotId: data.entrepotId,
dateDebut: data.dateDebut,
dateFin: data.dateFin
})
return reservation
}
Subscriber (Inventory Service)
// packages/microservices/inventory/src/application/listeners/ReservationListener.ts
export class ReservationListener {
constructor(
private inventoryService: InventoryService,
private eventBus: IEventBus
) {
this.setupListeners()
}
private setupListeners(): void {
this.eventBus.subscribe('reservation.confirmed', this.handleReservationConfirmed.bind(this))
this.eventBus.subscribe('reservation.cancelled', this.handleReservationCancelled.bind(this))
}
private async handleReservationConfirmed(event: ReservationConfirmedEvent): Promise<void> {
// Mettre à jour le stock
await this.inventoryService.reserveStock({
outilId: event.outilId,
entrepotId: event.entrepotId,
quantity: 1,
reservationId: event.reservationId
})
}
private async handleReservationCancelled(event: ReservationCancelledEvent): Promise<void> {
// Libérer le stock
await this.inventoryService.releaseStock({
reservationId: event.reservationId
})
}
}
Avantages
✅ Découplage : Services ne se connaissent pas directement
✅ Asynchrone : Pas de blocage (fire-and-forget)
✅ Scalabilité : Plusieurs subscribers possibles
✅ Résilience : Si un subscriber échoue, les autres continuent
✅ Audit Trail : Tous les events sont tracés
Récapitulatif des Patterns
| Pattern | Objectif | Couche | Exemple |
|---|---|---|---|
| Repository | Abstraction accès données | Infrastructure | SupabaseReservationRepository |
| Service Layer | Logique métier | Application | ReservationService |
| Dependency Injection | Découplage | Tous | Container IoC (inversify) |
| Factory | Création objets complexes | Domain | ReservationFactory |
| Observer (Event Bus) | Communication async | Infrastructure | Supabase Realtime Channels |
Architecture Complète avec Patterns
┌─────────────────────────────────────────────────┐
│ API Routes (Next.js API) │
│ (Thin Controllers) │
└──────────────────┬──────────────────────────────┘
│ Injection DI
┌──────────▼──────────┐
│ Service Layer │ ← Logique métier
│ (ReservationService)│
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Repository Pattern │ ← Abstraction
│ (IReservationRepo) │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Infrastructure │ ← Implémentation
│ (SupabaseRepo) │
└─────────────────────┘
┌─────────────────────┐
│ Factory Pattern │ ← Création entités
└─────────────────────┘
┌─────────────────────┐
│ Observer Pattern │ ← Events async
│ (Event Bus) │
└─────────────────────┘
Conséquences
✅ Avantages
- Testabilité maximale : Chaque composant testable isolément
- Maintenabilité : Code organisé et découplé
- Évolutivité : Facile d'ajouter features sans casser existant
- Réutilisabilité : Services et repositories réutilisables
- Démonstration : Montre maîtrise des patterns modernes
⚠️ Complexités
- Boilerplate : Plus de code (interfaces, implémentations)
- Courbe d'apprentissage : Patterns à comprendre
- Over-engineering : Risque si mal appliqué
🛠️ Mitigations
- Documentation : Bien expliquer chaque pattern
- Exemples : Fournir des templates pour chaque pattern
- Progressif : Appliquer patterns seulement où c'est utile
Alternatives Rejetées
❌ Pas de Patterns (Code procédural)
Raisons du rejet :
- Pas de découplage
- Difficile à tester
- Logique métier dispersée
- Maintenance cauchemar
❌ Patterns complexes (CQRS, Event Sourcing, DDD complet)
Raisons du rejet :
- Trop complexe pour projet académique
- Overhead important
- Courbe d'apprentissage élevée
- Pas nécessaire pour cette échelle
Références
- Repository Pattern
- Service Layer Pattern
- Dependency Injection
- Factory Pattern
- Observer Pattern
- Clean Architecture
Validations
- Repository interfaces définies
- Service Layer implémenté
- DI Container configuré (inversify)
- Factory patterns identifiés
- Event Bus testé avec Supabase Realtime
- Tests unitaires avec mocks validés
Prochaine révision : Après 1 sprint de développement (feedback sur complexité)