You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

587 lines
17 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

# 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** :
1. **Repository Pattern** (Accès aux données)
2. **Service Layer Pattern** (Logique métier)
3. **Dependency Injection (DI)** (Découplage)
4. **Factory Pattern** (Création d'objets)
5. **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)**
```typescript
// 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)**
```typescript
// 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
```typescript
// 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**
```typescript
// 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) ❌**
```typescript
class ReservationService {
private repo = new SupabaseReservationRepository() // Hard-coded !
}
```
**Après (Good) ✅**
```typescript
class ReservationService {
constructor(private repo: IReservationRepository) {} // Interface !
}
```
#### Implémentation avec Container
**Container DI**
```typescript
// 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**
```typescript
// 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
```typescript
// 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**
```typescript
// 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**
```typescript
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**
```typescript
// 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)**
```typescript
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)**
```typescript
// 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
1. **Testabilité maximale** : Chaque composant testable isolément
2. **Maintenabilité** : Code organisé et découplé
3. **Évolutivité** : Facile d'ajouter features sans casser existant
4. **Réutilisabilité** : Services et repositories réutilisables
5. **Démonstration** : Montre maîtrise des patterns modernes
### ⚠️ Complexités
1. **Boilerplate** : Plus de code (interfaces, implémentations)
2. **Courbe d'apprentissage** : Patterns à comprendre
3. **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](https://martinfowler.com/eaaCatalog/repository.html)
- [Service Layer Pattern](https://martinfowler.com/eaaCatalog/serviceLayer.html)
- [Dependency Injection](https://martinfowler.com/articles/injection.html)
- [Factory Pattern](https://refactoring.guru/design-patterns/factory-method)
- [Observer Pattern](https://refactoring.guru/design-patterns/observer)
- [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
---
## Validations
- [x] Repository interfaces définies
- [x] Service Layer implémenté
- [x] DI Container configuré (inversify)
- [x] Factory patterns identifiés
- [x] Event Bus testé avec Supabase Realtime
- [x] Tests unitaires avec mocks validés
---
**Prochaine révision** : Après 1 sprint de développement (feedback sur complexité)