Martin et le Pattern Factory Method¶
Partons à la découverte du "Factory Method", Pattern de Création issu du livre Design Patterns: Elements of Reusable Object-Oriented Software, en suivant Martin, fin bricoleur, qui aide ses amis à construire leurs ordinateurs.
Dans le dernier article1, je présentais le concept général des Design Patterns. Aujourd'hui, regardons l'histoire de Martin, qui assemble des ordinateurs. Martin va être confronté à un problème qu'il va résoudre à l'aide du pattern Factory Method (ou Fabrique simple en français).
Le contexte de Martin¶
Martin souhaite se procurer un ordinateur. Il va dans son magasin d'électronique favori où il prend des puces, des condensateurs, et tout un tas de machins et de bidules très précis (Martin est très fort). Puis, il rentre chez lui et soude les puces avec les machins et les bidules.
Et là : il obtient un ordinateur qui fonctionne parfaitement (Martin est très fort).
flowchart LR
Martin -.-> OM(Ordinateur<br> de<br> Martin)
Mais après le succès, vient la rançon du succès : Jacques trouve que l'ordinateur de Martin est très bien. Il voudrait exactement le même et demande à Martin les instructions pour le construire aussi.
Jacques (qui est aussi très fort) s'exécute et obtient son ordinateur.
flowchart LR
Martin -.-> OM(Ordinateur<br> de<br> Martin)
Jacques -.-> OJ(Ordinateur<br> de<br> Jacques)
Le schéma se reproduit : Alice, Elie, et Omar veulent également le même ordinateur. Ils sont tous très doués et reproduisent parfaitement les instructions de Martin.
flowchart LR
Martin -.-> OM(Ordinateur<br> de<br> Martin)
Jacques -.-> OJ(Ordinateur<br> de<br> Jacques)
flowchart LR
Alice -.-> OA(Ordinateur<br> de<br> Alice)
Elie -.-> OE(Ordinateur<br> de<br> Elie)
flowchart LR
Omar -.-> OO(Ordinateur<br> de<br> Omar)
Mais Martin est embêté. Le système de watercooling qu'il a mis en place sur son ordinateur (Martin est très fort) ne fonctionne pas parfaitement. Faisant ses essais, il parvient à un résultat qui lui convient et met à jour ses instructions de montage. Mais personne ne l'écoute et les ordinateurs fabriqués suivant ses instructions de montage erronés, circulent toujours dans la nature.
Par ailleurs, ses amis lui remontent que les ordinateurs qu'ils utilisent ne leur conviennent pas tous :
- Alice voudrait un PC optimisé pour jouer
- Omar n'a pas besoin d'autant de puissance ; il n'utilise que les outils bureautiques
- Jacques et Elie sont satisfaits de leurs machines
Le problème de Martin¶
Martin peut être assimilé à Framework : il est responsable des produits (ordinateur) que les autres produisent à l'aide de ses instructions. Pour conserver de la cohérence dans le temps et éviter la duplication des instructions de montage, Martin ne peut plus se permettre de continuer à travailler comme il le fait actuellement.
De plus la diversité des produits demandés par ses amis va lui demander de s'organiser pour répondre à leurs besoins.
Martin a donc :
- des produits multiples et similaires (sinon identiques), créés par des acteurs indépendants
- une cohérence à assurer sur les produits dont il est responsable
- besoin de se projeter dans une mise à jour, sans douleur, de la manière sont créés ses produits
Martin adopte une solution¶
Pour répondre à son problème de cohérence, Martin va commencer par centraliser la construction les ordinateurs.
Sur cette étape, Martin n'utilise pas de Design Pattern, mais plutôt le principe de programmation "DRY" (Don't Repeat Yourself (dans le contexte)). Il mutualise ce qui a de sens et s'assure que les ordinateurs créés répondent à ses critères de qualité. Puis, il s'organisera pour répondre aux demandes spécifiques d'Alice et Omar.
Martin établit alors une boutique d'ordinateur et propose une interface Fournisseur d'ordinateurs
que ses amis peuvent utiliser pour obtenir une machine.
Derrière la boutique, c'est toujours MArtin qui s'occupe de créer les ordinateurs : Martin implémente l'interface.
D'ailleurs, toutes les machines partagent des caractéristiques communes : elles implémentent une même interface Ordinateur
.
Martin rassemble les ordinateurs de ses amis sous un même concept : l'OrdinateurArtisanal.
Comme il s'agit d'une information dont ils n'ont pas besoin, ils vont utiliser l'interface Ordinateur
Pourquoi utiliser des interfaces ?
Une interface est queqlque chose qui permet d'introduire des abstractions dans le code.
Une abstraction est un gros mot, pour désigner une "simplification" du code, ou plutôt une séparation : on va séparer ce qu'on veut lire au niveau de l'abstraction, des détails qu'on veut masquer pour faciliter la lecture.
Les détails sont toujours là, mais on peut choisir de les lire à un autre moment.
Martin réalise qu'il a mis en place le Design Pattern : Factory Method. Il dispose des éléments suivants :
- Une interface
Ordinateur
décrivant un objet créé - Une interface
FournisseurDdinateurs
décrivant la possibilité de se procurer unOrdinateur
- Un objet concret
OrdinateurArtisanal
qui va réellement être utilisé par les amis de Martin - Une Fabrique concret
Martin
lui-même qui va construire lesOrdinateur Artisanaux
Mais... pour Alice et Omar ?¶
Dès lors que le code est mutualisé, il est facile de décliner les cas d'usage et de rajouter des comportements dédiés.
TODO TODO TODO Alice et Omar savent qu'ils veulent des ordinateurs spécifiques : ils sont prêts à s'adapter à Martin pour appeler les méthodes spécifiques qui leur fourniront des objets dédiés à leur contexte TODO TODO TODO
- Alice va utiliser une méthode
obtenirOrdinateurDeJeu
qui renvoie unOrdinateurDeJeu
avec des caractéristiques spécifiques - Omar va utiliser une méthode
obtenirOrdinateurDeBureautique
qui renvoie unOrdinateurDeBureautique
avec des caractéristiques spécifiques
Martin va pouvoir traiter ce type de demandes à sa manière :
- Il va lui-même assembler l'ordinateur de jeu pour Alice (car ça l'amuse)
- Il va acheter un ordinateur de seconde main à une
Brocante
pour l'ordinateur de bureautique
classDiagram
class FournisseurDOrdinateurs {
Ordinateur +obtenirOrdinateur()
OrdinateurDeJeu +obtenirOrdinateurDeJeu()
OrdinateurDeBureautique +obtenirOrdinateurDeBureautique()
}
class Martin {
Ordinateur +obtenirOrdinateur()
OrdinateurDeJeu +obtenirOrdinateurDeJeu()
}
class Brocante {
OrdinateurDeBureautique +obtenirOrdinateurDeBureautique()
}
class Ordinateur
class OrdinateurArtisanal
class OrdinateurDeJeu
class OrdinateurDeJeuArtisanal
class OrdinateurDeBureautique
class OrdinateurDeBureautiqueDeSecondeMain
FournisseurDOrdinateurs <|-- Martin
FournisseurDOrdinateurs<|-- Brocante
Ordinateur <|-- OrdinateurArtisanal
Ordinateur <|-- OrdinateurDeJeu
OrdinateurDeJeu <|-- OrdinateurDeJeuArtisanal
Ordinateur <|-- OrdinateurDeBureautique
OrdinateurDeBureautique <|-- OrdinateurDeBureautiqueDeSecondeMain
Martin --> OrdinateurArtisanal
Martin --> OrdinateurDeJeuArtisanal
Brocante --> OrdinateurDeBureautiqueDeSecondeMain
Ordinateur -- Martin
Ordinateur -- Jacques
OrdinateurDeJeu -- Alice
Ordinateur -- Elie
OrdinateurDeBureautique -- Omar
A quoi ça sert ?¶
Le Pattern Factory Method est, pour moi, le Pattern le plus simple à mettre en œuvre lorsque l'on souhaite séparer les responsabilités de son code. Il encapsule la création d'un objet derrière une fonction (un couple "interface + méthode" dans la littérature).
Apprendre à reconnaître quand et pourquoi on en a besoin, me semble une première étape importante pour améliorer ses compétences en conception logicielle.
Méthode de création vs. Factory Méthod
Déplacer le code de création d'un objet derrière une méthode, n'aboutis pas forcément à implémenter le Design Pattern : Factory Method.
Exemple: Les méthodes de création statique de certains objet, comme Integer.valueOf()
en Java, sont des méthodes de création, mais ne correspondent pas au pattern (et c'est OK).
Avantages¶
Le Design Pattern : Factory Method permet de :
- Mutualiser du code dans un contexte applicatif (respect du principe DRY). Cela permet de mieux gérer la cohérence des objets que l'on construit et de les faire évoluer de manière plus pérenne.
- N'exposer au client que l'interface dont il a besoin. La complexité de la création de l'objet n'a pas besoin d'être exposée.
- Découpler le code de création d'un objet du code qui l'utilise. Même s'ils sont couplés, Ces deux contextes peuvent vivre et évoluer séparément
- Par le nommage de la méthode de création, décrire fonctionnement l'objet créé. Cela donne dui sens à ce que l'on manipule, et permet au client de bien décrire ce dont il a besoin
Un autre avantage, c'est que je le trouve très simple à mettre en œuvre. Lorsque du code contient du code de création, il suffit de le refactorer de la manière suivante :
- Extraire le code de création dans une méthode (on n'implémente pas encore le pattern).
- Extraire la méthode dans un objet dédié et instancier cet objet avec les dépendances du code qui l'utilise (Fabrique concrète)
- Extraire une interface qui présente la méthode de création et utiliser cette interface à la place de votre fabrique concrète. La fabrique concrète est toujours instanciée dans les dépendances.
- Si vous faites de l'injection de dépendance, extraire la fabrique concrète là où vous déclarez vos injections.
Et il est possible de s'arrêter à n'importe laquelle des étapes décrites ci-dessus. Chaque petit pas vers un code amélioré est bon à prendre. Il n'est pas nécessaire d'aller systématiquement au bout de l'implémentation de ce pattern pour en tirer des bénéfices.
Inconvénients¶
Lorsque les fabriques concrètes ne contiennent pas d'intelligence, elles ne servent alors que d'indirection vers les constructeurs des objets concrets (ce qui a tout de même l'avantage de permettre de découpler le code).
Dans quel cadre je l'utilise¶
Dès que je crée un objet, je crée une méthode qui encapsule sa création.
Quelques Factories célèbres¶
En Java¶
String value = String.of(anyObject);
List<T> aList = List.of(object1, object2, object3, ...);
Collection<T> anyCollection = any;
Stream<T> aStream = anyCollection.stream();
En JavaScript¶
Cela m'arrive souvent d'utiliser le framework Redux. Redux utilise des objets, appelés Actions, pour déclencher des traitements et la mise à jour de l'état de l'application. Pour simplifier l'utilisation de ces actions et mutualiser leur création, je passe par des Factory Methods.
Dans quel cadre je ne l'utilise pas¶
Parfois, lorsque l'instanciation d'un objet est fortement couplée avec le contexte dans lequel il est utilisé, je ne l'utilise pas.
Par exemple, lorsque l'on utilise le Pattern Decorator, on souhaite souvent manipuler l'objet décoré (implémentation concrète) plutôt qu'une interface plus abstraite.
Et voilà2
Le Design Pattern : Factory Method, est l'un des 5 Pattern de Création (Abstract Factory
, Builder
, Factory Method
, Prototype
, Singleton
).
Il s'agit de toutes les mécaniques ayant trait à la construction des objets, et pas uniquement dans la Programmation-Orientée-Objet (POO, ou OOP en anglais).
Ces Patterns apportent de la flexibilité au code, en mettant en évidence les questions :
- Qu'est-ce qui est créé ?
- Qui le crée ?
- Comment cela est créé ?
- Quand cela est créé ?
Vous savez tout (ce que je sais) sur le Design Pattern : Factory Method.
Merci de m'avoir lu et bonne journée 🌞
Fabien
Bibliographie et liens utiles
- 🔗 Factory Method - Vince Huston : des schémas, des explications et des exemples de C++ et Java sur les Design Patterns du Gof.
- 🔗 Fabrique - Refactoring Guru : en français et toujours très complets même s'il y a beaucoup de redite du GoF,
-
sur ce site : "Les Design Patterns : les Gammes de la conception logicielle" ↩
-
En français dans le texte ↩