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
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
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
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)
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
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
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
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.