Factory Method et Abstract Factory en Java
1. Aperçu général
Factory Method et Abstract Factory font tous deux partie du groupe des creational patterns, ils vous aident tous deux à créer des objets sans avoir besoin de faire new directement sur une classe concrète. Cependant, ces deux patterns résolvent des problèmes différents et il est très facile de les confondre quand on débute.
Dans cet article, vous verrez la différence à travers l’exemple de PaymentMethod (Factory Method) et un système de création d’UI selon le thème (Abstract Factory). Après cette lecture, vous saurez quand vous avez besoin d’une simple factory, et quand vous avez besoin de toute une “usine qui fabrique des usines”.
2. C’est quoi Factory Method ?
Factory Method définit une méthode pour créer un objet, mais laisse aux sous-classes décider quelle classe concrète sera créée. En d’autres termes : vous avez une méthode interface create(), chaque sous-classe la redéfinit pour retourner une implémentation différente.
2.1. Le problème à résoudre
Supposons que vous ayez un système de paiement avec plusieurs types : CreditCardPayment, MomoPayment, BankTransferPayment. Si le code client fait new directement :
public void processOrder(Order order, String type) {
PaymentMethod payment;
if (type.equals("CREDIT_CARD")) {
payment = new CreditCardPayment();
} else if (type.equals("MOMO")) {
payment = new MomoPayment();
} else {
payment = new BankTransferPayment();
}
payment.pay(order.getAmount());
}
À chaque ajout d’un nouveau type de paiement, vous devez modifier ce if-else partout où il est utilisé. C’est précisément le code smell (mauvaise odeur de code) que Factory Method résout.
2.2. Application de Factory Method
public interface PaymentMethod {
void pay(BigDecimal amount);
}
public class CreditCardPayment implements PaymentMethod {
@Override
public void pay(BigDecimal amount) {
System.out.println("Paiement de " + amount + " par carte de crédit");
}
}
public class MomoPayment implements PaymentMethod {
@Override
public void pay(BigDecimal amount) {
System.out.println("Paiement de " + amount + " par Momo");
}
}
Explication : PaymentMethod est l’interface commune, chaque implémentation ne gère que la logique de paiement qui lui est propre. C’est le “produit” (product) que la factory créera.
Ensuite vient la classe abstraite contenant la Factory Method :
public abstract class PaymentProcessor {
// Factory Method - la sous-classe décide quelle instance créer
protected abstract PaymentMethod createPaymentMethod();
public void processOrder(Order order) {
PaymentMethod payment = createPaymentMethod();
payment.pay(order.getAmount());
}
}
public class CreditCardProcessor extends PaymentProcessor {
@Override
protected PaymentMethod createPaymentMethod() {
return new CreditCardPayment();
}
}
public class MomoProcessor extends PaymentProcessor {
@Override
protected PaymentMethod createPaymentMethod() {
return new MomoPayment();
}
}
createPaymentMethod()est la Factory Method : c’est une méthode abstraite, chaque sous-classe la redéfinit pour retourner le bon type dePaymentMethoddont elle a besoin.processOrder()est le “template” utilisé partout, sans avoir besoin de connaître le détail du type de paiement qui est créé.
Le client l’utilise comme ceci :
PaymentProcessor processor = new MomoProcessor();
processor.processOrder(new Order(new BigDecimal("150000")));
Paiement de 150000 par Momo
2.3. Tester Factory Method
class PaymentProcessorTest {
@Test
void shouldCreateCorrectPaymentMethod() {
PaymentProcessor processor = new CreditCardProcessor();
PaymentMethod method = processor.createPaymentMethod();
assertThat(method).isInstanceOf(CreditCardPayment.class);
}
}
Note : Pour que le test puisse appeler
createPaymentMethod()(protected), placez la classe de test dans le même package quePaymentProcessor, ou changez la méthode enpublicsi vous devez la tester de l’extérieur.
3. C’est quoi Abstract Factory ?
Abstract Factory fournit une interface pour créer une famille (family) d’objets liés, sans avoir besoin de spécifier la classe concrète de chaque objet. Contrairement à Factory Method qui crée un seul type de produit, Abstract Factory crée plusieurs produits qui doivent “aller ensemble”.
3.1. Le problème à résoudre
Supposons que vous faciez un système de thème d’interface utilisateur : un thème “Light” et un thème “Dark”. Chaque thème a besoin de son propre Button et de sa propre Checkbox, et ces deux éléments doivent être cohérents stylistiquement ensemble — vous ne pouvez pas mélanger un LightButton avec un DarkCheckbox.
public interface Button {
void render();
}
public interface Checkbox {
void render();
}
public class LightButton implements Button {
@Override
public void render() {
System.out.println("Afficher bouton avec fond blanc, texte noir");
}
}
public class DarkButton implements Button {
@Override
public void render() {
System.out.println("Afficher bouton avec fond noir, texte blanc");
}
}
public class LightCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Afficher case à cocher avec bordure gris clair");
}
}
public class DarkCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Afficher case à cocher avec bordure blanche");
}
}
3.2. Définir Abstract Factory
public interface UiComponentFactory {
Button createButton();
Checkbox createCheckbox();
}
public class LightThemeFactory implements UiComponentFactory {
@Override
public Button createButton() {
return new LightButton();
}
@Override
public Checkbox createCheckbox() {
return new LightCheckbox();
}
}
public class DarkThemeFactory implements UiComponentFactory {
@Override
public Button createButton() {
return new DarkButton();
}
@Override
public Checkbox createCheckbox() {
return new DarkCheckbox();
}
}
UiComponentFactory est l’interface qui définit toute une famille de produits (Button + Checkbox). Chaque implémentation (LightThemeFactory, DarkThemeFactory) s’assure que les produits créés sont toujours cohérents en termes de thème.
3.3. Utiliser Abstract Factory dans le client
public class ThemedApplication {
private final UiComponentFactory factory;
public ThemedApplication(UiComponentFactory factory) {
this.factory = factory;
}
public void renderUi() {
Button button = factory.createButton();
Checkbox checkbox = factory.createCheckbox();
button.render();
checkbox.render();
}
}
ThemedApplication app = new ThemedApplication(new DarkThemeFactory());
app.renderUi();
Afficher bouton avec fond noir, texte blanc
Afficher case à cocher avec bordure blanche
ThemedApplication ne sait pas et n’a pas besoin de savoir quelle classe concrète est créée. Elle dépend simplement de l’interface UiComponentFactory, donc changer de thème ne demande que de changer la factory qui est injectée.
3.4. Tester Abstract Factory
class ThemedApplicationTest {
@Test
void shouldCreateConsistentComponentsForDarkTheme() {
UiComponentFactory factory = new DarkThemeFactory();
Button button = factory.createButton();
Checkbox checkbox = factory.createCheckbox();
assertThat(button).isInstanceOf(DarkButton.class);
assertThat(checkbox).isInstanceOf(DarkCheckbox.class);
}
}
Ce test confirme la propriété la plus importante d’Abstract Factory : les produits créés à partir de la même factory appartiennent toujours à la même “famille”, pas de mélange entre Light et Dark.
4. Comparaison entre Factory Method et Abstract Factory
| Critère | Factory Method | Abstract Factory |
|---|---|---|
| Nombre de types de produit | 1 type de produit (PaymentMethod) |
Plusieurs types de produits liés (Button, Checkbox) |
| Mécanisme | Redéfinir une méthode dans la sous-classe | Composer plusieurs factory methods dans une interface |
| Relation | Inheritance (héritage) | Généralement composition (injection de factory) |
| Objectif | Libérer le client de la connaissance de la classe concrète d’un objet | Garantir la cohérence entre plusieurs objets liés |
Un moyen simple de s’en souvenir : Abstract Factory est généralement implémentée avec un ensemble de Factory Methods. Dans l’exemple ci-dessus, createButton() et createCheckbox() dans UiComponentFactory sont en fait deux Factory Methods internes.
4.1. Quand utiliser Factory Method
- Vous avez besoin de créer un seul type d’objet, mais la logique de création est complexe ou varie selon le contexte.
- Vous travaillez avec une hiérarchie de classes et voulez que les sous-classes décident elles-mêmes quel type d’objet enfant elles utiliseront (le template method pattern va souvent de pair avec ce pattern).
4.2. Quand utiliser Abstract Factory
- Vous avez besoin de créer plusieurs objets liés et devez garantir qu’ils sont compatibles entre eux (même thème, même fournisseur de base de données, même plateforme…).
- Le système peut accueillir l’ajout d’une nouvelle “famille de produits” (par exemple ajouter
HighContrastThemeFactory) sans vouloir modifier le code client.
Note : N’utilisez pas Abstract Factory si vous n’avez qu’un seul type de produit — ce serait créer une couche d’interface inutile. Factory Method simple suffit.
5. Intégration avec Spring Boot
Dans Spring, vous ne programmez que rarement la Factory Method/Abstract Factory selon la structure exacte de GoF, car le conteneur Spring joue déjà le rôle de factory. Cependant, bien comprendre l’idée vous aide à concevoir vos beans plus intelligemment.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
@Configuration
public class PaymentFactoryConfig {
@Bean
public Map<String, PaymentMethod> paymentMethods() {
return Map.of(
"CREDIT_CARD", new CreditCardPayment(),
"MOMO", new MomoPayment()
);
}
}
@Service
public class PaymentService {
private final Map<String, PaymentMethod> paymentMethods;
public PaymentService(Map<String, PaymentMethod> paymentMethods) {
this.paymentMethods = paymentMethods;
}
public void pay(String type, BigDecimal amount) {
PaymentMethod method = paymentMethods.get(type);
if (method == null) {
throw new IllegalArgumentException("Type de paiement non supporté : " + type);
}
method.pay(amount);
}
}
Ici, le conteneur Spring joue le rôle de véritable “usine” : il crée les beans PaymentMethod à l’avance et le client ne fait que chercher selon la clé. C’est une variante pragmatique de Factory Method dans l’environnement Spring — vous n’avez pas besoin d’écrire une classe XxxProcessor distincte pour chaque type de paiement.
6. Erreurs courantes
- Confondre Factory Method avec static factory method (comme
Optional.of(),List.of()). Une static factory method est juste une façon plus concise de créer un objet que le constructeur, sans rapport avec le polymorphisme du pattern GoF. - Abuser d’Abstract Factory quand on n’a qu’un seul produit : si vous n’avez que
ButtonsansCheckboxqui va avec, Abstract Factory est sur-ingénierie. - Laisser la factory en savoir trop sur la logique métier : une factory ne doit gérer que la création d’objets, elle ne devrait pas contenir la logique de traitement des commandes, de validation, etc.
- Oublier d’assurer la cohérence dans Abstract Factory : si vous permettez au client de choisir le
Buttond’une factory A et laCheckboxd’une factory B, vous avez cassé le but principal du pattern.
7. Conclusion
Factory Method convient quand vous devez déléguer la création d’un objet aux sous-classes, ce qui rend le code extensible selon le principe Open/Closed. Abstract Factory va plus loin : elle garantit qu’une famille d’objets liés est toujours créée de manière cohérente, évitant le mélange de composants incompatibles.
En pratique avec Spring Boot, vous ne programmez rarement manuellement selon la structure GoF, mais vous tirez profit du conteneur DI qui fait office de factory. Cependant, bien maîtriser l’essence de ces deux patterns vous aide à comprendre le code des bibliothèques (par exemple DocumentBuilderFactory, ConnectionFactory en JDBC) et à mieux concevoir vos modules extensibles.