Le Pattern Factory (Fabrique d'Objets)

Introduction

Le pattern Factory est un design pattern de création qui fournit une interface pour créer des objets sans spécifier explicitement leur classe concrète. Il encapsule la logique de création d'objets et permet une plus grande flexibilité dans le code.

Types de Factory Patterns

@startuml
package "Factory Patterns" {
  [Simple Factory] as simple
  [Factory Method] as method
  [Abstract Factory] as abstract
}

note right of simple
  Méthode statique simple
  Pas un vrai pattern GoF
end note

note right of method
  Méthode virtuelle
  Sous-classes décident
end note

note right of abstract
  Familles d'objets
  Cohérence garantie
end note
@enduml

Pourquoi Préférer la Factory au Traditionnel new ?

1. Nommer Plus Clairement ce que l'on Souhaite Créer

Les constructeurs par défaut ne permettent pas de préciser ce que l'on veut réellement créer. Il faut analyser les paramètres pour comprendre.

❌ Avec constructeurs :

// Il faut analyser les paramètres pour savoir le constructeur à utiliser
new ReponseHttp(Statut.OK, reponse);
new ReponseHttp(Statut.ERROR, error);

✅ Avec factory methods :

// On comprend immédiatement ce que l'on crée - code expressif
ReponseHttp.reponseOk(Reponse reponse);
ReponseHttp.reponseKo(Error error);
ReponseHttp.reponseAccepted();

Exemple du JDK :

// La javadoc du constructeur préconise l'utilisation de la méthode factory
new BigInteger(int bitLength, int certainty, Random rnd);  // ❌ Peu clair

BigInteger.probablePrime(bitLength, rnd);  // ✅ Clair et expressif

@startuml
participant Client
participant ReponseHttp

== Avec Constructeur (peu clair) ==
Client -> ReponseHttp: new ReponseHttp(Statut.OK, reponse)
note right: Quel type de réponse ?\nIl faut lire les paramètres

== Avec Factory (clair) ==
Client -> ReponseHttp: reponseOk(reponse)
note right: Immédiatement compréhensible\nCode auto-documenté
@enduml

2. Pas Obligé de Créer un Nouvel Objet à Chaque Appel

Contrairement aux constructeurs qui créent toujours une nouvelle instance, les factory methods peuvent : - Retourner des instances pré-construites - Implémenter un cache - Réutiliser des instances (pattern Flyweight)

Avantages : - Favorise l'immutabilité - Améliore les performances - Meilleur contrôle des instances

Exemple avec Boolean :

public final class Boolean {
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    public static Boolean valueOf(boolean b) {
        return b ? TRUE : FALSE;  // Réutilise les instances existantes
    }

    public static Boolean valueOf(String s) {
        return parseBoolean(s) ? TRUE : FALSE;
    }
}

Utilisation :

Boolean b1 = Boolean.valueOf(true);
Boolean b2 = Boolean.valueOf(true);
System.out.println(b1 == b2);  // true - même instance !

// Vs constructeur
Boolean b3 = new Boolean(true);
Boolean b4 = new Boolean(true);
System.out.println(b3 == b4);  // false - instances différentes

3. Retourner le Type ou un de ses Sous-types

Les factory methods peuvent retourner n'importe quelle sous-classe du type de retour déclaré.

Exemple avec Collections :

public class Collections {
    // Retourne différentes implémentations (non publiques)
    public static <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;  // EmptyList (privée)
    }

    public static <T> List<T> singletonList(T o) {
        return new SingletonList<>(o);  // SingletonList (privée)
    }

    public static <T> List<T> unmodifiableList(List<? extends T> list) {
        return new UnmodifiableList<>(list);  // UnmodifiableList (privée)
    }

    public static <T> List<T> synchronizedList(List<T> list) {
        return new SynchronizedList<>(list);  // SynchronizedList (privée)
    }
}

Avantages : - Les implémentations concrètes sont cachées - Flexibilité pour changer l'implémentation - API plus simple et épurée

@startuml
interface List<T>

class Collections {
  +{static} emptyList(): List
  +{static} singletonList(T): List
  +{static} unmodifiableList(List): List
  +{static} synchronizedList(List): List
}

class EmptyList
class SingletonList
class UnmodifiableList
class SynchronizedList

List <|.. EmptyList
List <|.. SingletonList
List <|.. UnmodifiableList
List <|.. SynchronizedList

Collections ..> EmptyList : <<creates>>
Collections ..> SingletonList : <<creates>>
Collections ..> UnmodifiableList : <<creates>>
Collections ..> SynchronizedList : <<creates>>

note right of Collections
  Les implémentations concrètes
  sont privées et cachées
  au client
end note
@enduml

4. Adapter la Classe Retournée Suivant les Paramètres

La factory peut retourner différentes implémentations selon les paramètres d'entrée.

Exemple avec EnumSet :

public abstract class EnumSet<E extends Enum<E>> {

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);

        if (universe.length <= 64) {
            return new RegularEnumSet<>(elementType, universe);  // Optimisé pour <= 64 éléments
        } else {
            return new JumboEnumSet<>(elementType, universe);    // Pour > 64 éléments
        }
    }

    public static <E extends Enum<E>> EnumSet<E> of(E e) {
        EnumSet<E> result = noneOf(e.getDeclaringClass());
        result.add(e);
        return result;
    }
}

Utilisation :

// Le client ne sait pas quelle implémentation est utilisée
EnumSet<DayOfWeek> weekend = EnumSet.of(DayOfWeek.SATURDAY, DayOfWeek.SUNDAY);
// Retourne RegularEnumSet car DayOfWeek a 7 valeurs (< 64)

@startuml
abstract class EnumSet<E> {
  +{static} noneOf(Class): EnumSet
  +{static} of(E...): EnumSet
}

class RegularEnumSet {
  -long elements
}

class JumboEnumSet {
  -long[] elements
}

EnumSet <|-- RegularEnumSet
EnumSet <|-- JumboEnumSet

note right of RegularEnumSet
  Utilisé pour les enums
  avec <= 64 valeurs
  (optimisé avec un long)
end note

note right of JumboEnumSet
  Utilisé pour les enums
  avec > 64 valeurs
  (utilise un tableau)
end note
@enduml

Simple Factory (Factory Statique)

Principe

La Simple Factory utilise une méthode statique pour créer des objets. Ce n'est pas un pattern GoF officiel, mais très utilisé.

Implémentation

public class ReponseHttp {
    private final Statut statut;
    private final Object contenu;

    // Constructeur privé
    private ReponseHttp(Statut statut, Object contenu) {
        this.statut = statut;
        this.contenu = contenu;
    }

    // Factory methods statiques
    public static ReponseHttp reponseOk(Reponse reponse) {
        return new ReponseHttp(Statut.OK, reponse);
    }

    public static ReponseHttp reponseKo(Error error) {
        return new ReponseHttp(Statut.ERROR, error);
    }

    public static ReponseHttp reponseAccepted() {
        return new ReponseHttp(Statut.ACCEPTED, null);
    }

    public static ReponseHttp reponseNotFound() {
        return new ReponseHttp(Statut.NOT_FOUND, null);
    }

    // Getters...
}

Utilisation :

// Code clair et expressif
ReponseHttp reponse1 = ReponseHttp.reponseOk(data);
ReponseHttp reponse2 = ReponseHttp.reponseKo(new Error("Erreur serveur"));
ReponseHttp reponse3 = ReponseHttp.reponseNotFound();

Conventions de Nommage

Les factory methods suivent généralement ces conventions :

Convention Signification Exemple
from Conversion de type Date.from(instant)
of Agrégation de paramètres EnumSet.of(MONDAY, FRIDAY)
valueOf Alternative à from et of Boolean.valueOf(true)
instance / getInstance Retourne une instance (peut être réutilisée) Calendar.getInstance()
create / newInstance Garantit une nouvelle instance Array.newInstance(classObj, length)
getType Comme getInstance mais dans une autre classe Files.getFileStore(path)
newType Comme newInstance mais dans une autre classe Files.newBufferedReader(path)

Exemples du JDK :

// from - conversion
Instant instant = Instant.now();
Date date = Date.from(instant);

// of - agrégation
Set<String> set = Set.of("a", "b", "c");
List<Integer> list = List.of(1, 2, 3);

// valueOf - conversion de String
Integer num = Integer.valueOf("123");
Boolean bool = Boolean.valueOf("true");

// getInstance - peut retourner une instance partagée
Calendar cal = Calendar.getInstance();
Logger logger = Logger.getLogger("MyLogger");

// newInstance - garantit une nouvelle instance
Class<?> clazz = String.class;
Object obj = clazz.getDeclaredConstructor().newInstance();

Factory Method Pattern

Principe

Le Factory Method définit une interface pour créer un objet, mais laisse les sous-classes décider quelle classe instancier.

Diagramme UML

@startuml
abstract class Creator {
  +operation(): void
  #{abstract} factoryMethod(): Product
}

class ConcreteCreatorA {
  #factoryMethod(): Product
}

class ConcreteCreatorB {
  #factoryMethod(): Product
}

interface Product {
}

class ConcreteProductA {
}

class ConcreteProductB {
}

Creator <|-- ConcreteCreatorA
Creator <|-- ConcreteCreatorB

Product <|.. ConcreteProductA
Product <|.. ConcreteProductB

Creator ..> Product : <<uses>>
ConcreteCreatorA ..> ConcreteProductA : <<creates>>
ConcreteCreatorB ..> ConcreteProductB : <<creates>>

note right of Creator
  La classe abstraite définit
  la factory method que les
  sous-classes implémentent
end note
@enduml

Implémentation

// Produit
public interface Document {
    void open();
    void save();
    void close();
}

// Produits concrets
public class WordDocument implements Document {
    @Override
    public void open() {
        System.out.println("Ouverture document Word");
    }

    @Override
    public void save() {
        System.out.println("Sauvegarde document Word");
    }

    @Override
    public void close() {
        System.out.println("Fermeture document Word");
    }
}

public class PdfDocument implements Document {
    @Override
    public void open() {
        System.out.println("Ouverture document PDF");
    }

    @Override
    public void save() {
        System.out.println("Sauvegarde document PDF");
    }

    @Override
    public void close() {
        System.out.println("Fermeture document PDF");
    }
}

// Créateur abstrait
public abstract class Application {

    // Factory Method - à implémenter par les sous-classes
    protected abstract Document createDocument();

    // Utilise la factory method
    public void newDocument() {
        Document doc = createDocument();
        doc.open();
        // Autres opérations...
    }
}

// Créateurs concrets
public class WordApplication extends Application {
    @Override
    protected Document createDocument() {
        return new WordDocument();
    }
}

public class PdfApplication extends Application {
    @Override
    protected Document createDocument() {
        return new PdfDocument();
    }
}

Utilisation :

Application wordApp = new WordApplication();
wordApp.newDocument();  // Crée et ouvre un WordDocument

Application pdfApp = new PdfApplication();
pdfApp.newDocument();  // Crée et ouvre un PdfDocument

Diagramme de Séquence

@startuml
participant Client
participant WordApplication
participant WordDocument

Client -> WordApplication: newDocument()
activate WordApplication

WordApplication -> WordApplication: createDocument()
activate WordApplication
create WordDocument
WordApplication -> WordDocument: new WordDocument()
WordApplication --> WordApplication: document
deactivate WordApplication

WordApplication -> WordDocument: open()
activate WordDocument
WordDocument --> WordApplication
deactivate WordDocument

WordApplication --> Client
deactivate WordApplication
@enduml

Abstract Factory Pattern

Principe

L'Abstract Factory fournit une interface pour créer des familles d'objets liés sans spécifier leurs classes concrètes.

Diagramme UML

@startuml
interface AbstractFactory {
  +createProductA(): AbstractProductA
  +createProductB(): AbstractProductB
}

class ConcreteFactory1 {
  +createProductA(): AbstractProductA
  +createProductB(): AbstractProductB
}

class ConcreteFactory2 {
  +createProductA(): AbstractProductA
  +createProductB(): AbstractProductB
}

interface AbstractProductA
interface AbstractProductB

class ProductA1
class ProductA2
class ProductB1
class ProductB2

AbstractFactory <|.. ConcreteFactory1
AbstractFactory <|.. ConcreteFactory2

AbstractProductA <|.. ProductA1
AbstractProductA <|.. ProductA2
AbstractProductB <|.. ProductB1
AbstractProductB <|.. ProductB2

ConcreteFactory1 ..> ProductA1 : <<creates>>
ConcreteFactory1 ..> ProductB1 : <<creates>>
ConcreteFactory2 ..> ProductA2 : <<creates>>
ConcreteFactory2 ..> ProductB2 : <<creates>>

note right of AbstractFactory
  Crée des familles d'objets
  cohérentes (A1 avec B1,
  A2 avec B2)
end note
@enduml

Implémentation

// Produits abstraits
public interface Button {
    void render();
    void onClick();
}

public interface Checkbox {
    void render();
    void onCheck();
}

// Produits concrets Windows
public class WindowsButton implements Button {
    @Override
    public void render() {
        System.out.println("Rendu bouton Windows");
    }

    @Override
    public void onClick() {
        System.out.println("Clic bouton Windows");
    }
}

public class WindowsCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Rendu checkbox Windows");
    }

    @Override
    public void onCheck() {
        System.out.println("Check checkbox Windows");
    }
}

// Produits concrets Mac
public class MacButton implements Button {
    @Override
    public void render() {
        System.out.println("Rendu bouton Mac");
    }

    @Override
    public void onClick() {
        System.out.println("Clic bouton Mac");
    }
}

public class MacCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("Rendu checkbox Mac");
    }

    @Override
    public void onCheck() {
        System.out.println("Check checkbox Mac");
    }
}

// Factory abstraite
public interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// Factories concrètes
public class WindowsFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new WindowsCheckbox();
    }
}

public class MacFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new MacButton();
    }

    @Override
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }
}

// Application utilisant la factory
public class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

    public void render() {
        button.render();
        checkbox.render();
    }
}

Utilisation :

// Détection du système d'exploitation
String os = System.getProperty("os.name").toLowerCase();
GUIFactory factory;

if (os.contains("win")) {
    factory = new WindowsFactory();
} else if (os.contains("mac")) {
    factory = new MacFactory();
} else {
    factory = new WindowsFactory();  // Par défaut
}

// L'application utilise la factory appropriée
Application app = new Application(factory);
app.render();  // Rendu cohérent selon l'OS

Bonnes Pratiques

1. Rendre les Constructeurs Privés ou Protégés

public class User {
    private User() {
        // Constructeur privé
    }

    public static User createAdmin(String name) {
        // ...
    }

    public static User createGuest() {
        // ...
    }
}

2. Utiliser des Noms Expressifs

// ✅ Bon - noms clairs
LocalDate.of(2024, 1, 1);
Optional.empty();
Collections.emptyList();

// ❌ Mauvais - noms génériques
LocalDate.create(2024, 1, 1);
Optional.get();
Collections.list();

3. Documenter le Comportement

/**
 * Retourne une instance de Boolean.
 * Cette méthode peut retourner une instance partagée pour améliorer les performances.
 * 
 * @param b la valeur boolean
 * @return Boolean.TRUE si b est true, Boolean.FALSE sinon
 */
public static Boolean valueOf(boolean b) {
    return b ? TRUE : FALSE;
}

4. Valider les Paramètres

public static User createUser(String email, String password) {
    if (email == null || email.isEmpty()) {
        throw new IllegalArgumentException("Email requis");
    }
    if (password == null || password.length() < 8) {
        throw new IllegalArgumentException("Mot de passe trop court");
    }
    return new User(email, password);
}

Quand Utiliser Quel Pattern ?

Simple Factory (Méthode Statique)

Utilisez quand : - Vous voulez des noms expressifs pour la création - Vous voulez contrôler les instances (cache, singleton) - La logique de création est simple

Exemple : Boolean.valueOf(), Collections.emptyList()

Factory Method

Utilisez quand : - Vous voulez déléguer la création aux sous-classes - La classe ne connaît pas à l'avance les objets à créer - Vous voulez une hiérarchie de créateurs

Exemple : Frameworks (Spring, JUnit), parsers, lecteurs de fichiers

Abstract Factory

Utilisez quand : - Vous devez créer des familles d'objets cohérentes - Vous voulez garantir que les objets créés sont compatibles - Vous voulez isoler le code client des implémentations concrètes

Exemple : UI toolkits, drivers de base de données, thèmes d'application

Conclusion

Les patterns Factory offrent une grande flexibilité dans la création d'objets :

  • Nommage expressif : Code auto-documenté
  • Contrôle des instances : Cache, réutilisation, optimisation
  • Polymorphisme : Retour de sous-types
  • Flexibilité : Adaptation selon les paramètres
  • Découplage : Séparation de la création et de l'utilisation

Préférez les factory methods aux constructeurs pour les objets complexes ou lorsque le nom du constructeur ne suffit pas à exprimer l'intention.

Ressources