Design patterns en TypeScript - 01 - Factory : créer des objets sans new partout

Le pattern Factory en TypeScript : créer des objets complexes avec une fonction ou une classe. Exemples concrets et cas d'usage.

01 - Factory : créer des objets sans new partout

Ce que tu vas apprendre

  • La différence entre simple factory, factory method et abstract factory
  • Pourquoi la simple factory couvre 90% des cas en TypeScript
  • Quand une factory est utile et quand elle est de trop

Prerequisites

Avoir lu l'introduction de la serie.


Le problème

Tu as un objet complexe a créer. Pas un simple { name: "truc" }, un vrai objet avec de la config, de la validation, des dépendances a injecter. Et tu le créés a 4 endroits différents dans ton code. A chaque endroit, tu repetes la meme logique d'initialisation. Si tu oublies une étape a un endroit, bug en prod.

typescript// Eparpille dans 4 fichiers differents...
const session = new ImapSession(host, port);
session.setTimeout(30000);
session.setEncryption(await encrypt(password));
session.setPoolSize(5);
await session.connect();

Cinq lignes, quatre endroits. Vingt lignes de création dispersees. C'est pas un drame dans un petit projet, mais ca devient un cauchemar de maintenance quand tu changes le processus de création.

La simple factory (une fonction)

La solution la plus pragmatique en TypeScript. Une fonction qui encapsule la création :

typescriptinterface Logger {
  log(message: string): void;
  error(message: string): void;
}

class ConsoleLogger implements Logger {
  log(message: string) { console.log(`[LOG] ${message}`); }
  error(message: string) { console.error(`[ERR] ${message}`); }
}

class FileLogger implements Logger {
  constructor(private path: string) {}
  log(message: string) { appendFileSync(this.path, `[LOG] ${message}\n`); }
  error(message: string) { appendFileSync(this.path, `[ERR] ${message}\n`); }
}

function createLogger(env: "dev" | "prod"): Logger {
  if (env === "dev") return new ConsoleLogger();
  return new FileLogger("/var/log/app.log");
}

Un seul endroit qui décidé quel logger créer. Le reste du code appelle createLogger("prod") et n'a pas a savoir que FileLogger existe. Si tu ajoutes un ElasticLogger demain, tu modifies une seule fonction.

C'est pas un "vrai" pattern du GoF (les puristes diront que c'est un "Simple Factory" ou "Creation Method"), mais c'est ce que tu utiliseras 90% du temps en TypeScript. Pas besoin d'une hiérarchie de classes.

La factory method (le pattern du GoF)

La version classique du livre. Une classe abstraite definit une méthode de création, les sous-classes decident quoi créer :

typescriptabstract class NotificationSender {
  abstract createNotification(message: string): Notification;

  async send(userId: string, message: string): Promise<void> {
    const notif = this.createNotification(message);
    await notif.deliver(userId);
    await this.logDelivery(userId, notif);
  }

  private async logDelivery(userId: string, notif: Notification) {
    console.log(`Sent ${notif.type} to ${userId}`);
  }
}

class EmailNotificationSender extends NotificationSender {
  createNotification(message: string): Notification {
    return new EmailNotification(message, this.smtpConfig);
  }
}

class SmsNotificationSender extends NotificationSender {
  createNotification(message: string): Notification {
    return new SmsNotification(message, this.twilioClient);
  }
}

L'interet par rapport a la simple factory ? La méthode send contient de la logique commune (le logging). Les sous-classes ne changent que la création. En pratique, je l'utilise rarement parce qu'en TypeScript on préféré la composition a l'héritage.

L'abstract factory (famille d'objets)

L'Abstract Factory créé des familles d'objets lies. Imaginons un système de theming UI :

typescriptinterface UIFactory {
  createButton(label: string): Button;
  createInput(placeholder: string): Input;
  createModal(title: string): Modal;
}

class DarkThemeFactory implements UIFactory {
  createButton(label: string) { return new DarkButton(label); }
  createInput(placeholder: string) { return new DarkInput(placeholder); }
  createModal(title: string) { return new DarkModal(title); }
}

class LightThemeFactory implements UIFactory {
  createButton(label: string) { return new LightButton(label); }
  createInput(placeholder: string) { return new LightInput(placeholder); }
  createModal(title: string) { return new LightModal(title); }
}

Honnêtement ? En TypeScript backend, je n'ai jamais eu besoin d'Abstract Factory. C'est un pattern qui a du sens en Java ou C# avec des frameworks UI lourds. En frontend React, tu geres ca avec des composants et du CSS, pas avec une factory.

Exemple réel : la factory IMAP de paltemps.fr

Sur paltemps.fr, chaque compte mail a besoin d'une session IMAP pour lire les mails. Creer une session demande plusieurs étapes : résoudre le serveur, chiffrer le mot de passe stocke, configurer le TTL, ajouter la session au pool de connexions. Tout ca etait duplique dans trois endroits du code (synchro manuelle, synchro cron, preview).

La solution : une factory function.

typescriptinterface ImapSessionConfig {
  host: string;
  port: number;
  email: string;
  encryptedPassword: string;
}

async function createImapSession(
  account: MailAccount,
  pool: SessionPool,
): Promise<ImapSession> {
  const encrypted = await encrypt(account.password, account.salt);
  const session = new ImapSession({
    host: account.imapHost,
    port: account.imapPort,
    email: account.email,
    encryptedPassword: encrypted,
  });
  session.setTtl(300_000); // 5 minutes
  await session.connect();
  pool.add(session);
  return session;
}

Un seul endroit. Trois appelants. Si le processus de création change (par exemple ajouter un healthcheck apres la connexion), un seul fichier a modifier.

Quand ne PAS utiliser une factory

Si l'objet a un constructeur simple sans logique, new MyClass(arg) suffit. Emballer ca dans une factory ajoute un niveau d'indirection inutile.

typescript// Non. Juste... non.
function createUser(name: string, email: string): User {
  return new User(name, email);
}

// Appelle directement :
const user = new User("Nicolas", "nico@example.com");

Utilise une factory quand :

  • La création demande plusieurs étapes ou de la logique
  • Tu as besoin de choisir entre plusieurs implementations
  • La meme création est dupliquee a 3+ endroits

Ne l'utilise pas quand :

  • Le constructeur est trivial
  • Tu n'as qu'une seule implementation
  • L'objet est créé a un seul endroit

La regle que je donne aux juniors : si ta "factory" ne fait que return new X(args) sans aucune logique supplementaire, supprime-la.


Résumé

  • La simple factory (une fonction) couvre la grande majorite des cas en TypeScript
  • La factory method (héritage) est utile quand la logique de création varie mais le processus global est identique
  • L'abstract factory (famille d'objets) est rarement nécessaire en TypeScript
  • Une factory sans logique est une indirection inutile

Article précédent : 00 - Introduction

Article suivant : 02 - Repository : abstraire l'acces aux donnees

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.