Design Pattern
Structurels

Decorator

Ajouter du comportement à un objet sans modifier sa classe

Problème

On veut ajouter des fonctionnalités à un objet existant (logging, caching, validation, authentification) sans modifier sa classe ni créer une explosion de sous-classes.

Decorator de service

interface UserRepository {
  findById(id: string): Promise<User | null>;
  findAll(): Promise<User[]>;
  save(user: User): Promise<User>;
}

// Implémentation de base
class ApiUserRepository implements UserRepository {
  async findById(id: string) { /* appel API */ }
  async findAll() { /* appel API */ }
  async save(user: User) { /* appel API */ }
}

// Decorator : ajoute du cache sans modifier ApiUserRepository
class CachedUserRepository implements UserRepository {
  private cache = new Map<string, User>();

  constructor(private repository: UserRepository) {}

  async findById(id: string): Promise<User | null> {
    if (this.cache.has(id)) {
      return this.cache.get(id)!;
    }
    const user = await this.repository.findById(id);
    if (user) this.cache.set(id, user);
    return user;
  }

  async findAll(): Promise<User[]> {
    return this.repository.findAll();
  }

  async save(user: User): Promise<User> {
    const saved = await this.repository.save(user);
    this.cache.set(saved.id, saved); // met à jour le cache
    return saved;
  }
}

// Decorator : ajoute du logging
class LoggedUserRepository implements UserRepository {
  constructor(private repository: UserRepository, private logger: Logger) {}

  async findById(id: string) {
    this.logger.info(`findById: ${id}`);
    const result = await this.repository.findById(id);
    this.logger.info(`findById: ${id} → ${result ? 'found' : 'not found'}`);
    return result;
  }

  async findAll() {
    this.logger.info('findAll');
    return this.repository.findAll();
  }

  async save(user: User) {
    this.logger.info(`save: ${user.id}`);
    return this.repository.save(user);
  }
}

// Composition de decorators
const repository = new LoggedUserRepository(
  new CachedUserRepository(
    new ApiUserRepository()
  ),
  logger
);

Higher-Order Components en React

Les HOC sont le pattern Decorator appliqué aux composants React :

// HOC : ajoute une garde d'authentification
function withAuth<P extends object>(WrappedComponent: React.ComponentType<P>) {
  return function AuthGuard(props: P) {
    const { isAuthenticated, isLoading } = useAuth();

    if (isLoading) return <Spinner />;
    if (!isAuthenticated) return <Navigate to="/login" />;

    return <WrappedComponent {...props} />;
  };
}

// HOC : ajoute un boundary d'erreur
function withErrorBoundary<P extends object>(WrappedComponent: React.ComponentType<P>) {
  return function WithErrorBoundary(props: P) {
    return (
      <ErrorBoundary fallback={<ErrorMessage />}>
        <WrappedComponent {...props} />
      </ErrorBoundary>
    );
  };
}

// Composition de HOC
const ProtectedDashboard = withAuth(withErrorBoundary(Dashboard));

Custom hooks comme decorators

En React moderne, les custom hooks sont souvent préférés aux HOC pour décorer le comportement :

// Hook de base
function useUsers() {
  return useQuery({ queryKey: ['users'], queryFn: fetchUsers });
}

// "Decorator" : ajoute du logging autour du hook
function useUsersWithLogging() {
  const query = useUsers();

  useEffect(() => {
    if (query.isError) {
      logger.error('Failed to fetch users', query.error);
    }
  }, [query.isError, query.error]);

  return query;
}

TypeScript decorators

TypeScript supporte les décorateurs de classe (stage 3). Utiles avec des frameworks comme NestJS :

function Log(target: unknown, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = async function (...args: unknown[]) {
    console.log(`${key} called with`, args);
    const result = await original.apply(this, args);
    console.log(`${key} returned`, result);
    return result;
  };
  return descriptor;
}

class UserService {
  @Log
  async getUser(id: string): Promise<User> {
    return fetch(`/api/users/${id}`).then(r => r.json());
  }
}

Exercice : Decorator dans le Pokémon Builder

Contexte

Certains Pokémon méritent un traitement spécial : afficher un score de puissance calculé depuis les stats, ou marquer visuellement les Pokémon légendaires. Ajouter ces données directement dans le type Pokemon polluerait le modèle de base avec des informations qui ne concernent que l'affichage.

Objectif

Implémenter deux décorateurs fonctionnels qui enrichissent un Pokemon sans modifier son type de base.

Instructions

  1. Crée le fichier src/decorator/pokemonDecorators.ts.

  2. Définis un type DecoratedPokemon qui étend Pokemon avec deux champs optionnels :

    • powerLevel: number, somme de toutes les stats de base
    • isLegendary: boolean
  3. Implémente deux fonctions décorateur :

    • withPowerLevel(pokemon: Pokemon): DecoratedPokemon, calcule et ajoute le powerLevel
    • withLegendary(pokemon: Pokemon, legendaryIds: number[]): DecoratedPokemon, marque le Pokémon comme légendaire si son id est dans la liste
  4. Dans PokemonCard, affiche le powerLevel si présent.

  5. Dans App.tsx, applique les deux décorateurs aux Pokémon après le chargement.

Les ids légendaires de la génération 1 : [144, 145, 146, 150, 151].

Résultat attendu

La carte d'un Pokémon légendaire affiche un indicateur visuel et son score de puissance, sans que le type Pokemon ni la PokemonFactory n'aient été modifiés.