Design Pattern
Créationnels

Factory

Créer des objets sans coupler le code à leurs classes concrètes

Problème

Quand la création d'un objet dépend d'une condition (type, configuration, contexte), une suite de if/else ou de switch dans le code client devient difficile à maintenir. Le pattern Factory centralise cette logique de création.

Factory Function

La version la plus simple en TypeScript : une fonction qui retourne l'instance appropriée selon les paramètres.

type NotificationType = 'email' | 'sms' | 'push';

interface Notification {
  send(message: string, recipient: string): Promise<void>;
}

class EmailNotification implements Notification {
  async send(message: string, recipient: string) {
    await sendEmail({ to: recipient, body: message });
  }
}

class SmsNotification implements Notification {
  async send(message: string, recipient: string) {
    await sendSms({ to: recipient, text: message });
  }
}

class PushNotification implements Notification {
  async send(message: string, recipient: string) {
    await sendPush({ deviceId: recipient, message });
  }
}

// La factory
function createNotification(type: NotificationType): Notification {
  switch (type) {
    case 'email': return new EmailNotification();
    case 'sms': return new SmsNotification();
    case 'push': return new PushNotification();
  }
}

// Le code client ne connaît pas les classes concrètes
const notification = createNotification(userPreferences.notificationType);
await notification.send('Votre commande a été expédiée.', user.contact);

Factory avec registre

Pour des systèmes extensibles sans modifier la factory, on utilise un registre :

type NotificationConstructor = new () => Notification;

const registry = new Map<NotificationType, NotificationConstructor>();

registry.set('email', EmailNotification);
registry.set('sms', SmsNotification);
registry.set('push', PushNotification);

function createNotification(type: NotificationType): Notification {
  const Constructor = registry.get(type);
  if (!Constructor) throw new Error(`Unknown notification type: ${type}`);
  return new Constructor();
}

// Ajouter un type sans modifier la factory ni le switch
registry.set('slack', SlackNotification);

En React : factory de composants

Les factories de composants sont utiles pour rendre des listes d'éléments hétérogènes.

type BlockType = 'text' | 'image' | 'video' | 'quote';

interface Block {
  type: BlockType;
  content: unknown;
}

// Factory de composants
const blockComponents: Record<BlockType, React.ComponentType<{ block: Block }>> = {
  text: TextBlock,
  image: ImageBlock,
  video: VideoBlock,
  quote: QuoteBlock,
};

function BlockRenderer({ block }: { block: Block }) {
  const Component = blockComponents[block.type];
  if (!Component) return null;
  return <Component block={block} />;
}

// Dans une page de CMS
function Page({ blocks }: { blocks: Block[] }) {
  return (
    <main>
      {blocks.map((block, i) => (
        <BlockRenderer key={i} block={block} />
      ))}
    </main>
  );
}

Ajouter un nouveau type de bloc revient à ajouter une entrée dans blockComponents et créer le composant correspondant. Le Page ne change pas.

Abstract Factory

L'Abstract Factory crée des familles d'objets liés. Utile pour des thèmes ou des environnements différents :

interface UIFactory {
  createButton(): ButtonComponent;
  createInput(): InputComponent;
  createModal(): ModalComponent;
}

class DesktopUIFactory implements UIFactory {
  createButton() { return new DesktopButton(); }
  createInput() { return new DesktopInput(); }
  createModal() { return new DesktopModal(); }
}

class MobileUIFactory implements UIFactory {
  createButton() { return new MobileButton(); }
  createInput() { return new MobileInput(); }
  createModal() { return new MobileBottomSheet(); }
}

function createUI(platform: 'desktop' | 'mobile'): UIFactory {
  return platform === 'desktop' ? new DesktopUIFactory() : new MobileUIFactory();
}

Exercice : Factory dans le Pokémon Builder

Contexte

Dans App.tsx, la création d'un objet Pokemon à partir de la réponse brute de l'API est faite inline dans le Promise.all :

const parsed: Pokemon[] = details.map((d) => ({
  id: d.id,
  name: d.name,
  sprite: d.sprites.front_default,
  types: d.types.map((t) => t.type.name),
  stats: {
    hp: d.stats[0].base_stat,
    // ...
  },
}));

Cette logique de transformation est mélangée avec la logique de fetch et celle du composant. Si la structure de la réponse API change, ou si on veut créer un Pokémon autrement (depuis un fichier JSON local, depuis un test), il faut toucher App.tsx.

Objectif

Extraire la logique de création d'un Pokemon dans une PokemonFactory.

Instructions

  1. Crée le fichier src/factory/PokemonFactory.ts.

  2. Implémente une classe PokemonFactory avec une méthode statique create(raw: any): Pokemon qui prend la réponse brute de l'API et retourne un objet Pokemon typé.

  3. Dans App.tsx, remplace le mapping inline par PokemonFactory.create(d).

  4. Vérifie que les types TypeScript sont corrects et que l'application fonctionne.

Résultat attendu

App.tsx ne contient plus aucune logique de transformation de données. Si la structure de la réponse API change, seule PokemonFactory est à modifier.