Les dossiers s'empilaient. Des affaires de boutons, des histoires de barres de défilement, des querelles de palettes de couleurs. Chaque interface graphique avait son style – Windows gris et anguleux, macOS lisse et arrondi, Linux sobre et fonctionnel. Le code de l'application ressemblait à un champ de bataille : des if (OS === 'windows') { new BoutonWindows() } partout. Un cauchemar de cohérence. C'est là qu'il intervient, le gros calibre. L'Abstract Factory. Celui qui ne s'occupe pas d'un produit, mais d'une famille entière.
Familles d’objets
Une famille d'objets, c'est un ensemble de classes concrètes qui sont conçues pour fonctionner ensemble. Elles partagent un thème, une logique, une dépendance commune.
- Une interface utilisateur :
Bouton,Checkbox,BarreDeDefilementdans le style Windows. - La même interface :
Bouton,Checkbox,BarreDeDefilementdans le style macOS. - Une suite de connexions à des services cloud liés :
ClientStorage,ClientQueue,ClientDatabasepour le fournisseur AWS. - La même suite pour le fournisseur Google Cloud.
Le problème n'est pas de créer un bouton isolé. Le problème est de garantir que tous les composants d'une même interface utilisateur soient du même style. Qu'on n'ait pas un bouton Windows à côté d'une checkbox macOS. C'est une question de cohérence.
Cohérence des produits
C'est l'essence même du pattern. L'Abstract Factory garantit que les produits créés par une fabrique concrète sont compatibles entre eux. Ils sont faits pour travailler ensemble.
// LE CAUCHEMAR : Le manque de cohérence imposé manuellement.
let bouton: Bouton;
let checkbox: Checkbox;
if (theme === 'windows') {
bouton = new BoutonWindows();
// ... 50 lignes plus loin, dans un autre fichier...
checkbox = new CheckboxWindows(); // Ouf, on s'est souvenu.
} else if (theme === 'mac') {
bouton = new BoutonMac();
// ... Dans un service injecté dynamiquement...
checkbox = new CheckboxMac(); // Croisons les doigts.
}
// Un oubli, une erreur, et l'interface devient un monstre hybride.
L'Abstract Factory encapsule cette logique de choix. On ne choisit plus chaque produit individuellement. On choisit une usine, et cette usine nous fournit tous les produits de la famille, garantissant leur compatibilité.
Cas d’usage concrets
- Kits d'interface graphique (GUI) : L'exemple canonique. Créer des composants cohérents pour différents systèmes d'exploitation ou thèmes (sombre/clair).
- Abstraction de fournisseurs cloud : Votre application doit supporter AWS, Azure et GCP. Chaque fournisseur a ses propres implémentations pour le stockage, les files d'attente, les bases de données. Une Abstract Factory par fournisseur garantit qu'on utilise un ensemble cohérent de services.
- Support de différents moteurs/moteurs de rendu : Un jeu qui doit tourner sur DirectX et OpenGL. Une usine pour créer les objets de rendu (Shader, Texture, Buffer) spécifiques à chaque API.
- Génération de documents dans différents formats : Un système qui produit des rapports avec des éléments (En-tête, Corps, Graphique, Pied-de-page). Une usine pour le format PDF, une autre pour HTML, une pour DOCX. Chaque usine créera les objets de formatage adaptés.
Implémentation : Le cas du GUI
// 1. Interfaces des Produits Abstraits (la famille)
interface Bouton {
afficher(): string;
}
interface Checkbox {
cocher(): string;
}
// 2. Produits Concrets par Famille
// --- Famille Windows ---
class BoutonWindows implements Bouton {
afficher(): string { return 'Affiche un bouton style Windows (gris, relief)'; }
}
class CheckboxWindows implements Checkbox {
cocher(): string { return 'Coche une checkbox carrée style Windows'; }
}
// --- Famille macOS ---
class BoutonMac implements Bouton {
afficher(): string { return 'Affiche un bouton style macOS (lisse, arrondi)'; }
}
class CheckboxMac implements Checkbox {
cocher(): string { return 'Coche une checkbox ronde style macOS'; }
}
// 3. L'ABSTRACT FACTORY (interface)
interface GUIFactory {
creerBouton(): Bouton;
creerCheckbox(): Checkbox;
}
// 4. Factories Concrètes (une par famille cohérente)
class WindowsFactory implements GUIFactory {
creerBouton(): Bouton {
return new BoutonWindows(); // Garantit un produit Windows
}
creerCheckbox(): Checkbox {
return new CheckboxWindows(); // Garantit un produit Windows compatible
}
}
class MacFactory implements GUIFactory {
creerBouton(): Bouton {
return new BoutonMac(); // Garantit un produit Mac
}
creerCheckbox(): Checkbox {
return new CheckboxMac(); // Garantit un produit Mac compatible
}
}
// 5. Code Client - Il ne dépend que des interfaces abstraites.
class Application {
private bouton: Bouton;
private checkbox: Checkbox;
// On injecte l'usine. Le choix de la famille est fait ICI, une seule fois.
constructor(private factory: GUIFactory) {
this.bouton = factory.creerBouton();
this.checkbox = factory.creerCheckbox();
}
afficherUI() {
console.log(this.bouton.afficher());
console.log(this.checkbox.cocher());
// Tout est cohérent. Impossible de se tromper.
}
}
// 6. Configuration (souvent au point d'entrée de l'app)
const config = { os: 'mac' }; // Simulons une config
let factory: GUIFactory;
if (config.os === 'windows') {
factory = new WindowsFactory();
} else {
factory = new MacFactory();
}
const app = new Application(factory);
app.afficherUI();
// Si config.os = 'mac', affiche :
// "Affiche un bouton style macOS (lisse, arrondi)"
// "Coche une checkbox ronde style macOS"
// LA COHÉRENCE EST GARANTIE.
Avantages
- Cohérence garantie : C'est le grand avantage. L'usine concrète s'assure que vous ne mélangerez jamais des produits de familles incompatibles.
- Découplage client/produits concrets : Le code client (
Application) ne connaît que les interfacesBouton,CheckboxetGUIFactory. Il est totalement indépendant des classes concrètes (BoutonWindows,CheckboxMac, etc.). - Principe Ouvert/Fermé (OCP) : Pour ajouter une nouvelle famille (ex: un thème "Linux"), on crée un nouvel ensemble de produits concrets et une nouvelle fabrique concrète. On n'a pas besoin de modifier le code client ni les fabriques existantes.
- Principe de Responsabilité Unique (SRP) : La logique d'instanciation des produits est déléguée à un lieu unique : la fabrique concrète.
Contraintes
- Complexité : C'est un pattern lourd. Introduire une Abstract Factory pour une seule classe de produit est un overkill total (utilisez une Factory Method ou une Simple Factory).
- Rigidité pour étendre les familles : Ajouter un nouveau type de produit (ex: une nouvelle interface
Slider) à la famille est difficile. Il faut modifier l'interfaceGUIFactory(ajoutercreerSlider()), et par conséquent modifier toutes les factories concrètes existantes pour implémenter cette nouvelle méthode. Cela viole le principe OCP pour l'extension des types de produits. C'est la grande faiblesse. L'Abstract Factory est parfait pour ajouter de nouvelles familles de produits, mais pas pour ajouter de nouveaux types de produits à une famille existante. - Prolifération de classes : Le nombre de classes explose. Pour N familles et M types de produits, vous avez besoin de : M interfaces de produits + (N x M) classes concrètes de produits + 1 interface d'usine + N classes d'usines concrètes.
J'éteignis l'écran qui affichait les deux thèmes côte à côte. L'un cohérent, l'autre monstrueux. L'Abstract Factory, c'est l'arme de contrôle massive. Elle impose l'ordre, la cohérence, au prix d'une architecture plus lourde.
On ne le sort pas pour un pet de mouche. On le sort quand le risque d'incohérence est réel, coûteux, et que le système doit vivre avec plusieurs "univers" parallèles et bien définis.
Le Singleton était un flic solitaire. La Factory Method, un indicateur spécialisé. L'Abstract Factory, c'est le syndicat. Il contrôle tout un écosystème, garantit que les règles sont respectées à l'intérieur de son périmètre.
Le prochain pattern sur la liste serait différent. Moins hiérarchique, plus souple. Le Builder. Pour quand la création est un processus, une suite d'étapes, et non un choix parmi une famille.