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

# 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é)