Deuxième article d'une série consacrée aux Design Patterns. Aujourd'hui, le pattern AbstractFactory
où il sera question de produits, de familles et de fabriques (factories, au pluriel s'il vous plaît).
Le livre Head First Design Patterns (dont j'ai déjà vanté les mérites) regroupe les deux patterns Factory Method
et AbstractFactory
dans un même chapitre consultable en ligne et intitulé The Factory Pattern: Baking with OO Goodness. Je ne saurais trop vous encourager à le consulter !
Classification
Le pattern Abstract Factory
est classé dans la catégorie des Design Patterns de création.
Définition
Provide an interface for creating families of related or dependent objects without specifying their concrete classes.
C'est une très jolie définition dont je ne me lasse pas ... (Comme toute définition, elle paraît assez barbare tant que l'on n'a pas approfondi le concept, ce que je vous propose de faire dans les paragraphes qui suivent).
En résumé, le pattern Abstract Factory
va nous permettre d'instancier des familles de produits dépendant les uns des autres sans qu'il soit nécessaire de préciser leur type concret (je ne suis pas sûr qu'on soit plus avancé ...)
Schéma du design pattern Abstract Factory
Ne vous laissez pas impressionner par la densité du schéma et le nombre de participants. Le pattern n'a rien d'insurmontable et peut s'avérer utile dans de nombreuses situations.
Pour l'heure, et pour y voir un peu plus clair, je vous suggère de diviser mentalement le schéma en deux :
- à gauche figurent les fabriques
- à droite les produits à instancier
Les produits appartiennent à une famille. Sur ce schéma le produit A1 et le produit B1 appartiennent à la même famille et sont donc destinés à collaborer ensemble. Le produit A2 et le produit B2 appartiennent à une autre famille et sont également conçus pour collaborer ensemble. En revanche, A1 et B2 ne peuvent pas fonctionner ensemble. Il est donc important d'instancier des produits qui sont compatibles (autrement dit, qui appartiennent à la même famille) et c'est là qu'interviennent les fabriques abstraites.
Je suis sûr qu'au cours de votre carrière de développeur vous avez été amenés à modéliser des contraintes métiers énoncées peu ou prou ainsi : "si mon produit est une instance de A, alors c'est mon service A qui le gère ; si mon produit est une instance de B, alors c'est mon service B qui le gère". Ne cherchez pas plus loin, c'est justement à ce genre de problématique que répond le pattern Abstract Factory
.
Exemple de problèmes familiaux ...
J'ai développé une extension Chrome qui permet de présenter des statistiques structurées à partir du détail d'un commit sur Github : nom du projet, nom de l'auteur, liste des fichiers concernés par les modifications, nombre de lignes supprimées, nombre de lignes ajoutées, etc.
Pour ce faire, j'ai développé une librairie qui contient deux classes qui analysent le contenu HTML d'une page Github et en extraient les données pertinentes :
- la première classe
GithubCrawler
parse le DOM (cette classe connaît les chemins XPATH qui permettent d'extraire les données brutes au format HTML) - la seconde classe
GithubParser
sait parser les données brutes retournées par mon crawler pour en extraire les données épurées (débarrassées des tags HTML notamment) et les structurer
Mon client est aux anges et souhaite donc étendre ce fonctionnel aux projets hébergés sur Gitlab (vous la voyez arriver la nouvelle famille
?). Bien évidemment, la structure HTML des pages Gitlab est complètement différente des pages Github, et mon parser Github est tout à fait incapable de comprendre les données retournées par mon crawler Gitlab ... La contrainte est donc la suivante : si mon crawler est un crawler Github, alors je dois utiliser le parser Github ; si mon crawler est un crawler Gitlab, alors je dois utiliser le parser Gitlab. Et mon client ne compte pas s'arrêter là, il souhaite bien évidemment aussi gérer les pages Bitbucket ...
Résolution de la problématique à l'aide du DP AbstractFactory
Vous l'aurez sans doute deviné, nous nous trouvons en présence de trois familles de produits différentes : la famille des produits Github, la famille des produits Gitlab et la famille des produits Bickbucket. Dans chacune de ces familles, on retrouve un produit Crawler
et un produit Parser
conçus pour collaborer ensemble.
Comment garantir que j'utilise des produits d'une même famille ? Réponse : le pattern Abstract Factory. Ce qui nous donne le récapitulatif suivant :
Github | Gitlab | BitBucket | |
---|---|---|---|
SCMCrawlerInterface: |
|
||
GithubCrawler | GitlabCrawler | BitBucketCrawler | |
SCMParserInterface: |
|
||
GithubParser | GitlabParser | BitBucketParser | |
SCMFactoryInterface |
|
||
GithubFactory | GitlabFactory | BitBucketFactory |
Explication de la solution
- j'ai introduit une interface
SCMCrawlerInterface
qu'implémentent les crawlers de chaque famille - j'ai également introduit une interface
SCMParserInterface
pour les parsers - j'ai créé une fabrique (Factory) pour chaque famille :
- chaque fabrique implémente l'interface
SCMFactoryInterface
- nous implémentons autant de fabriques concrètes que de familles
- l'interface d'une fabrique expose autant de méthodes de création qu'il y a de produits dans une famille
- chaque fabrique implémente l'interface
Voici à titre d'exemple le code de la fabrique GithubFactory
(je vous épargne le code des deux autres fabriques):
<?php
class GithubFactory implements SCMFactoryInterface {
public function getCrawler(): SCMCrawlerInterface {
return new GithubCrawler();
}
public function getParser(): SCMParserInterface {
return new GithubParser();
}
}
A présent, en fonction du contexte dans lequel nous nous trouvons (Github, Gitlab ou Bitbucket), il suffit d'instancier la bonne fabrique et d'appeler respectivement ses méthodes getCrawler
et getParser
pour obtenir les bons services adaptés au contexte courant. C'est au final la fabrique qui est garante de la compatibilité des produits qui collaborent.
Autre avantage de cette solution : la résolution des services à instancier selon le contexte se fait une seule fois, il suffit d'instancier la bonne fabrique (nous n'avons pas à résoudre l'instanciation du bon crawler dans un premier temps, puis du bon parser dans un second temps).
Grâce à ce pattern, l'ajout d'une nouvelle famille de produits peut également se faire assez aisément (un nouveau SCM à gérer), tout comme l'ajout d'un nouveau produit dans chaque famille (on pourrait envisager d'implémenter un Renderer spécialisé dans chaque famille, chargé de l'affichage des données obtenues, par exemple).
Conclusion
Parce que ce sont tous deux des patterns de fabrique, on confond souvent la Factory Method
et le pattern Abstract Factory
. Voici donc un résumé de ces deux patterns en mettant l'accent sur ce qui les différencie :
Factory Method
:new
déporté dans une méthode dédiée, un seul type d'objet retourné à la fois (une seule méthode d'instanciation par Factory)Abstract Factory
: famille de produits liés fonctionnellement, plusieurs fabriques, plusieurs types d'objets retournés par chaque fabrique (plusieurs méthodes d'instanciation par Factory)