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