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.