# Hexagonal Architecture Guide

## Qu'est-ce que l'Architecture Hexagonale ?

L'architecture hexagonale (Ports & Adapters) a été inventée par **Alistair Cockburn** pour résoudre un problème fondamental: **la business logic est trop couplée à l'infrastructure**.

### Le Problème

Dans une architecture traditionnelle:

```typescript
// ❌ Antipattern: Business logic couplée à Prisma
export async function createUser(email: string, name: string) {
  const user = await prisma.user.create({
    data: { email, name }
  });
  return user;
}
```

**Problèmes**:
1. Impossible de tester sans database
2. Impossible de changer de DB sans réécrire le code
3. Impossible de réutiliser sur mobile (Prisma = Node.js only)
4. Business rules mélangées avec infrastructure

### La Solution: Hexagonal Architecture

```typescript
// ✅ Business logic isolée
export class CreateUser {
  constructor(private userRepository: IUserRepository) {}

  async execute(input: CreateUserInput): Promise<Result<User, DomainError>> {
    const emailResult = Email.create(input.email);
    if (isFailure(emailResult)) return emailResult;

    const userResult = User.create({
      id: crypto.randomUUID(),
      email: emailResult.value,
      name: input.name
    });

    if (isFailure(userResult)) return userResult;

    return this.userRepository.save(userResult.value);
  }
}
```

**Avantages**:
1. ✅ Testable sans DB (mock IUserRepository)
2. ✅ Peut utiliser Prisma, MongoDB, InMemory
3. ✅ Fonctionne sur Node.js, Browser, React Native
4. ✅ Business rules pures et isolées

## Concepts Clés

### 1. Ports (Interfaces)

Les **ports** sont des **interfaces** qui définissent les contrats:

```typescript
// packages/domain/src/repositories/IUserRepository.ts
export interface IUserRepository {
  findById(id: string): Promise<Result<User | null, DomainError>>;
  findByEmail(email: Email): Promise<Result<User | null, DomainError>>;
  save(user: User): Promise<Result<User, DomainError>>;
  delete(id: string): Promise<Result<void, DomainError>>;
}
```

**Règle**: Les ports sont définis **dans le domain** et implémentés **dans l'infrastructure**.

### 2. Adapters (Implémentations)

Les **adapters** sont des **implémentations concrètes** des ports:

```typescript
// packages/infrastructure/src/database/repositories/PrismaUserRepository.ts
export class PrismaUserRepository implements IUserRepository {
  constructor(private prisma: PrismaClient) {}

  async save(user: User): Promise<Result<User, DomainError>> {
    const json = user.toJSON();
    const data = await this.prisma.user.upsert({
      where: { id: user.getId() },
      update: { name: json.name, email: json.email },
      create: { id: json.id, name: json.name, email: json.email }
    });
    return this.toDomain(data);
  }

  // ... autres méthodes
}
```

**Possibilités d'adapters**:
- `PrismaUserRepository` (PostgreSQL via Prisma)
- `MongoUserRepository` (MongoDB)
- `InMemoryUserRepository` (Tests)
- `SupabaseUserRepository` (Supabase)

### 3. Domain (Cœur)

Le **domain** contient la business logic pure:

```typescript
// packages/domain/src/entities/User.ts
export class User {
  private constructor(private props: UserProps) {}

  static create(props: Omit<UserProps, 'createdAt' | 'updatedAt'>): Result<User, ValidationError> {
    // Business rules
    if (!props.name || props.name.trim().length < 2) {
      return failure(new ValidationError('Name must be at least 2 characters'));
    }

    return success(new User({
      ...props,
      createdAt: new Date(),
      updatedAt: new Date()
    }));
  }

  updateName(name: string): Result<void, ValidationError> {
    // Business rule
    if (!name || name.trim().length < 2) {
      return failure(new ValidationError('Name must be at least 2 characters'));
    }

    this.props.name = name;
    this.props.updatedAt = new Date();
    return success(undefined);
  }
}
```

**Caractéristiques**:
- ZERO dépendance externe
- Business rules explicites
- Testable sans infrastructure
- Platform-agnostic

## Dependency Inversion Principle

Le principe clé de l'architecture hexagonale est le **Dependency Inversion Principle** (DIP):

> Les modules de haut niveau ne doivent pas dépendre des modules de bas niveau.
> Les deux doivent dépendre d'abstractions.

```
❌ Dépendance directe:
Use-Case → PrismaRepository

✅ Inversion de dépendance:
Use-Case → IUserRepository ← PrismaRepository
```

### Exemple Concret

```typescript
// ❌ AVANT: Dépendance directe
import { PrismaClient } from '@prisma/client';

class CreateUser {
  async execute(email: string, name: string) {
    const prisma = new PrismaClient();
    const user = await prisma.user.create({ data: { email, name } });
    return user;
  }
}
```

**Problème**: `CreateUser` dépend de `PrismaClient`

```typescript
// ✅ APRÈS: Inversion de dépendance
interface IUserRepository {
  save(user: User): Promise<Result<User, DomainError>>;
}

class CreateUser {
  constructor(private userRepository: IUserRepository) {}

  async execute(input: CreateUserInput): Promise<Result<User, DomainError>> {
    const userResult = User.create({ id: crypto.randomUUID(), email: input.email, name: input.name });
    if (isFailure(userResult)) return userResult;

    return this.userRepository.save(userResult.value);
  }
}
```

**Solution**: `CreateUser` dépend de `IUserRepository` (interface)

## Testing avec Hexagonal Architecture

### Unit Tests (Domain)

```typescript
describe('CreateUser Use-Case', () => {
  it('should create user with valid input', async () => {
    // Mock repository
    const mockRepository: IUserRepository = {
      save: vi.fn().mockResolvedValue(success(user)),
      findById: vi.fn(),
      findByEmail: vi.fn().mockResolvedValue(success(null)),
      delete: vi.fn()
    };

    const useCase = new CreateUser(mockRepository);
    const result = await useCase.execute({ email: 'test@example.com', name: 'John' });

    expect(isSuccess(result)).toBe(true);
  });
});
```

**Avantages**:
- Pas besoin de database
- Tests rapides (< 1ms)
- Isolés et déterministes

### Integration Tests (Infrastructure)

```typescript
describe('PrismaUserRepository', () => {
  beforeEach(async () => {
    await prisma.user.deleteMany();
  });

  it('should save and retrieve user', async () => {
    const repository = new PrismaUserRepository(prisma);
    const email = Email.create('test@example.com');
    const user = User.create({ id: '123', email: email.value, name: 'John' });

    await repository.save(user.value);
    const result = await repository.findById('123');

    expect(isSuccess(result)).toBe(true);
    if (isSuccess(result)) {
      expect(result.value?.getName()).toBe('John');
    }
  });
});
```

## Injection de Dépendances

### Dans l'Application Layer

```typescript
// packages/api/src/routers/user.ts
import { prisma } from '@crafted/database';
import { PrismaUserRepository } from '@crafted/infrastructure';

const userRepository = new PrismaUserRepository(prisma);

export const userRouter = router({
  create: publicProcedure.input(createUserSchema).mutation(async ({ input }) => {
    const useCase = new CreateUser(userRepository);
    const result = await useCase.execute(input);

    if (isFailure(result)) {
      throw new TRPCError({ code: 'BAD_REQUEST', message: result.error.message });
    }

    return result.value.toJSON();
  })
});
```

### Avec Dependency Injection Container (avancé)

```typescript
// Composition root
const container = {
  userRepository: new PrismaUserRepository(prisma),
  productRepository: new PrismaProductRepository(prisma),

  createUser: () => new CreateUser(container.userRepository),
  getUserById: () => new GetUserById(container.userRepository)
};

// Usage
const useCase = container.createUser();
const result = await useCase.execute(input);
```

## Exemples Pratiques

### Exemple 1: Changer de Database

```typescript
// Avant: PostgreSQL avec Prisma
const userRepository = new PrismaUserRepository(prisma);

// Après: MongoDB
const userRepository = new MongoUserRepository(mongoClient);

// Le domain reste IDENTIQUE ✅
const useCase = new CreateUser(userRepository);
```

### Exemple 2: Tests avec InMemory

```typescript
class InMemoryUserRepository implements IUserRepository {
  private users = new Map<string, User>();

  async save(user: User): Promise<Result<User, DomainError>> {
    this.users.set(user.getId(), user);
    return success(user);
  }

  async findById(id: string): Promise<Result<User | null, DomainError>> {
    const user = this.users.get(id);
    return success(user ?? null);
  }
}

// Tests ultra-rapides
const repository = new InMemoryUserRepository();
const useCase = new CreateUser(repository);
```

### Exemple 3: Platform-Agnostic

```typescript
// ✅ Fonctionne sur Node.js
import { PrismaUserRepository } from '@crafted/infrastructure';
const repository = new PrismaUserRepository(prisma);

// ✅ Fonctionne sur Browser (IndexedDB)
import { IndexedDBUserRepository } from '@crafted/infrastructure-web';
const repository = new IndexedDBUserRepository();

// ✅ Fonctionne sur React Native (AsyncStorage)
import { AsyncStorageUserRepository } from '@crafted/infrastructure-mobile';
const repository = new AsyncStorageUserRepository();

// Le use-case reste IDENTIQUE ✅
const useCase = new CreateUser(repository);
```

## Anti-Patterns à Éviter

### ❌ 1. Importer l'infrastructure dans le domain

```typescript
// ❌ MAUVAIS
import { PrismaClient } from '@prisma/client';

export class User {
  async save() {
    const prisma = new PrismaClient();
    await prisma.user.update({ ... });
  }
}
```

### ❌ 2. Business logic dans l'infrastructure

```typescript
// ❌ MAUVAIS
export class PrismaUserRepository {
  async save(user: User) {
    // ❌ Business rule dans l'infrastructure
    if (user.name.length < 2) {
      throw new Error('Name too short');
    }

    await this.prisma.user.create({ ... });
  }
}
```

### ❌ 3. Anemic Domain Model

```typescript
// ❌ MAUVAIS: Pas de comportement
export interface User {
  id: string;
  email: string;
  name: string;
}

// Toute la logique est ailleurs
export function updateUserName(user: User, name: string) {
  if (name.length < 2) throw new Error('...');
  user.name = name;
}
```

## Checklist Hexagonal Architecture

✅ **Domain Layer**
- [ ] ZERO dépendance externe
- [ ] Entities avec comportement (pas d'anemic models)
- [ ] Value objects immutables
- [ ] Repository interfaces (pas d'implémentations)
- [ ] Result pattern (pas de throw)
- [ ] 100% test coverage

✅ **Application Layer**
- [ ] Orchestration ultra-fine (< 10 lignes)
- [ ] Injection de dépendances
- [ ] Mapping errors domain → HTTP

✅ **Infrastructure Layer**
- [ ] Implémente les interfaces du domain
- [ ] Conversion domain ↔ database
- [ ] Tests d'intégration

## Ressources

- [Hexagonal Architecture par Alistair Cockburn](https://alistair.cockburn.us/hexagonal-architecture/)
- [DDD par Eric Evans](https://www.domainlanguage.com/)
- [Clean Architecture par Uncle Bob](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
