Aller au contenu principal

Concept #003

Design Patterns

Le Design Pattern Factory

#0037 min de lecture
  • design-patterns
  • architecture
  • programmation
La Factory : Arrêtez d'utiliser new partout !

Le Design Pattern Factory en une phrase

Factory est un patron de conception dont la responsabilité unique est de créer des objets. Votre code métier ne demande plus "Je veux tel objet", mais "Donne-moi quelque chose qui fait ça". L'usine décide quoi instancier. Le reste de l'application s'en fiche, tant que ça marche.

C'est l'un des patterns les plus utilisés, mais souvent mal compris. Son mantra : "Arrêtez d'utiliser le mot-clé new partout !" Déléguez la création de vos objets pour garder un code souple, testable et prêt à évoluer.


Le problème du new

À première vue, new Paypal() semble anodin. Mais chaque fois que vous écrivez ce mot-clé directement dans votre code métier, vous créez une dépendance forte et invisible.

Imaginez une application qui gère des paiements. Elle contient des dizaines de fichiers, chacun faisant new Paypal() pour initier une transaction. Tout fonctionne. Puis un jour, votre contrat avec PayPal change, ou vous voulez proposer Stripe à vos clients européens.

Résultat ? Vous partez à la chasse aux occurrences. Un grep dans le projet révèle 23 endroits différents où new Paypal() est instancié. Chaque modification est risquée : oubliez-en un seul, et un bug de production pointe le bout de son nez.

Le vrai problème n'est pas PayPal. C'est que le code qui utilise le service de paiement connaît aussi le détail de son implémentation. C'est un couplage fort déguisé en code innocent.


La Solution : la Factory

La Factory introduit un intermédiaire dont la seule mission est de créer des objets. Votre code métier ne fait plus new Paypal() — il demande à la Factory "donne-moi un processeur de paiement", et c'est la Factory qui choisit lequel instancier.

Avant :

// Couplage fort partout dans le code
class CheckoutService {
  processPayment(amount: number) {
    const gateway = new Paypal(); // Dépendance directe et concrète
    gateway.charge(amount);
  }
}

Après :

// Le code métier ne sait plus ce qu'il reçoit, juste ce qu'il peut faire
class CheckoutService {
  constructor(private gateway: PaymentGateway) {}
 
  processPayment(amount: number) {
    this.gateway.charge(amount); // Dépend d'une abstraction
  }
}

La Factory s'occupe de la création. Le service métier s'occupe du métier. Chacun son rôle.


Exemple concret : une Factory de passerelles de paiement

Voici comment implémenter ce pattern en TypeScript pour un système de paiement multi-fournisseurs.

1. Définir le contrat commun (l'interface)

// Le contrat que toutes les passerelles doivent respecter
interface PaymentGateway {
  charge(amount: number, currency: string): Promise<PaymentResult>;
  refund(transactionId: string): Promise<void>;
}
 
interface PaymentResult {
  transactionId: string;
  status: "success" | "failure";
}

2. Implémenter les fournisseurs concrets

class PaypalGateway implements PaymentGateway {
  async charge(amount: number, currency: string): Promise<PaymentResult> {
    console.log(`[PayPal] Facturation de ${amount} ${currency}`);
    // Logique spécifique à l'API PayPal
    return { transactionId: "pp_" + Date.now(), status: "success" };
  }
 
  async refund(transactionId: string): Promise<void> {
    console.log(`[PayPal] Remboursement de ${transactionId}`);
  }
}
 
class StripeGateway implements PaymentGateway {
  async charge(amount: number, currency: string): Promise<PaymentResult> {
    console.log(`[Stripe] Facturation de ${amount} ${currency}`);
    // Logique spécifique à l'API Stripe
    return { transactionId: "ch_" + Date.now(), status: "success" };
  }
 
  async refund(transactionId: string): Promise<void> {
    console.log(`[Stripe] Remboursement de ${transactionId}`);
  }
}

3. La Factory : un seul endroit pour décider

type GatewayType = "paypal" | "stripe";
 
class PaymentGatewayFactory {
  static create(type: GatewayType): PaymentGateway {
    switch (type) {
      case "paypal":
        return new PaypalGateway();
      case "stripe":
        return new StripeGateway();
      default:
        throw new Error(`Passerelle de paiement inconnue : ${type}`);
    }
  }
}

4. Le code client — simple et découplé

// Le service ne sait pas ce qu'il reçoit. Il sait juste ce qu'il peut faire.
const gatewayType = process.env.PAYMENT_PROVIDER as GatewayType;
const gateway = PaymentGatewayFactory.create(gatewayType);
 
const result = await gateway.charge(99.99, "EUR");
console.log(`Transaction : ${result.transactionId}`);

Pour passer de PayPal à Stripe, il suffit de changer une variable d'environnement. Aucune ligne de code métier ne bouge.


Combo Factory + Feature Flag

La Factory est l'endroit parfait pour placer un Feature Flag. C'est l'un des cas d'usage les plus puissants du pattern.

Imaginez que vous développez une nouvelle version de votre moteur de recommandation. Elle est prête, mais vous voulez la déployer progressivement : d'abord 10% de vos utilisateurs, puis 50%, puis 100%. Sans Factory, ce type de logique conditionnel se retrouve éparpillé partout dans le code.

Avec une Factory, cette décision est centralisée en un seul endroit :

interface RecommendationEngine {
  getRecommendations(userId: string): Promise<Product[]>;
}
 
class LegacyRecommendationEngine implements RecommendationEngine {
  async getRecommendations(userId: string): Promise<Product[]> {
    // Ancien algorithme basé sur les achats passés
    return fetchByPurchaseHistory(userId);
  }
}
 
class MLRecommendationEngine implements RecommendationEngine {
  async getRecommendations(userId: string): Promise<Product[]> {
    // Nouveau modèle de machine learning
    return fetchByMLModel(userId);
  }
}
 
class RecommendationEngineFactory {
  static create(userId: string): RecommendationEngine {
    // La Factory consulte le Feature Flag
    const isInBeta = featureFlagService.isEnabled("ml-recommendations", userId);
 
    if (isInBeta) {
      return new MLRecommendationEngine();
    }
 
    return new LegacyRecommendationEngine();
  }
}

Le reste de l'application ne sait même pas qu'il existe deux versions. Il appelle la Factory, récupère un moteur, et l'utilise. Quand le Feature Flag est activé à 100%, on retire la branche Legacy de la Factory et on supprime l'ancienne implémentation. Migration terminée, sans aucune friction dans le code métier.


Factory et le principe Open/Closed

Le Design Pattern Factory est l'une des implémentations les plus naturelles du principe Open/Closed : ouvert à l'extension, fermé à la modification.

Reprenons notre exemple de passerelles de paiement. Demain, vous devez ajouter Apple Pay. Voici tout ce que vous faites :

1. Vous créez une nouvelle classe ApplePayGateway qui implémente PaymentGateway.

2. Vous ajoutez un case "apple-pay" dans votre Factory.

C'est tout. Le CheckoutService, le OrderService, et tous les autres services qui utilisent une PaymentGateway n'ont pas bougé d'un iota. Vous avez étendu le système sans modifier le code existant.

// Ajout d'un nouveau cas : seule la Factory est modifiée
class PaymentGatewayFactory {
  static create(type: GatewayType): PaymentGateway {
    switch (type) {
      case "paypal":
        return new PaypalGateway();
      case "stripe":
        return new StripeGateway();
      case "apple-pay":           // Nouvelle ligne
        return new ApplePayGateway(); // Nouvelle classe, rien d'autre ne change
      default:
        throw new Error(`Passerelle de paiement inconnue : ${type}`);
    }
  }
}

C'est la force du pattern : la complexité de création est encapsulée, et le reste du code reste stable.


Quand utiliser la Factory ?

La Factory brille dans plusieurs situations :

  • Plusieurs implémentations d'une même interface — Paiements, notifications (email/SMS/push), moteurs de rendu, connecteurs de base de données. Si vous avez N fournisseurs qui font la même chose, une Factory s'impose.

  • La logique de création est complexe — Si instancier un objet nécessite plusieurs étapes, de la configuration, ou l'assemblage de dépendances, centralisez ça dans une Factory plutôt que de le dupliquer partout.

  • Vous anticipez l'évolution — Si vous savez que de nouveaux types vont s'ajouter dans le temps (nouveaux fournisseurs, nouveaux algorithmes), une Factory rend ces ajouts chirurgicaux.

  • Les tests — Une Factory facilite l'injection de mocks en test. Au lieu de stubber des constructeurs, vous passez une Factory de test qui retourne des doubles.

Quand ne pas l'utiliser : Pour un objet créé à un seul endroit, sans variante, une Factory est de la sur-ingénierie. Restez pragmatique. Le pattern doit résoudre un vrai problème, pas impressionner vos collègues.


Résumé

Sans FactoryAvec Factory
new Paypal() éparpillé partoutUn seul endroit de création
Couplage fort aux implémentations concrètesDépendance aux abstractions (interfaces)
Migration = chasse aux occurrencesMigration = modifier la Factory
Feature Flags dans le code métierFeature Flags centralisés dans la Factory
Violation de l'Open/Closed PrincipleRespect naturel de l'Open/Closed Principle

La Factory, c'est simple : déléguez la création. Votre code métier demande quoi faire, pas comment être construit. L'usine décide. Le reste du code, lui, s'en fiche.