
Le modèle de fabrique abstraite en génie logiciel est un modèle de conception qui permet de créer des familles d'objets liés sans imposer leurs classes concrètes, en encapsulant un groupe de fabriques individuelles qui ont un thème commun sans spécifier leurs classes concrètes. Selon ce modèle, un composant logiciel client crée une implémentation concrète de la fabrique abstraite, puis utilise l' interface générique de la fabrique pour créer les objets concrets qui font partie de la famille. Le client ne sait pas quels objets concrets il reçoit de chacune de ces fabriques internes, car il utilise uniquement les interfaces génériques de leurs produits. Ce modèle sépare les détails de l'implémentation d'un ensemble d'objets de leur utilisation générale et s'appuie sur la composition d'objets, car la création d'objets est implémentée dans des méthodes exposées dans l'interface de la fabrique.
L'utilisation de ce modèle permet d'obtenir des implémentations concrètes interchangeables sans modifier le code qui les utilise, même au moment de l'exécution . Cependant, l'utilisation de ce modèle, comme avec des modèles de conception similaires , peut entraîner une complexité inutile et un travail supplémentaire lors de l'écriture initiale du code. De plus, des niveaux plus élevés de séparation et d'abstraction peuvent donner lieu à des systèmes plus difficiles à déboguer et à maintenir.
Aperçu
Le modèle de conception d'usine abstraite est l'un des 23 modèles décrits dans le livre Design Patterns de 1994. Il peut être utilisé pour résoudre des problèmes tels que :
- Comment une application peut-elle être indépendante de la manière dont ses objets sont créés ?
- Comment une classe peut-elle être indépendante de la manière dont les objets dont elle a besoin sont créés ?
- Comment créer des familles d’objets liés ou dépendants ?
La création d'objets directement dans la classe qui requiert les objets n'est pas flexible. Cela implique que la classe est associée à des objets particuliers et rend impossible toute modification ultérieure de l'instanciation sans modifier la classe. Cela empêche la réutilisation de la classe si d'autres objets sont requis et rend la classe difficile à tester car les objets réels ne peuvent pas être remplacés par des objets fictifs.
Une fabrique est l'emplacement d'une classe concrète dans le code où les objets sont construits . L'implémentation du modèle vise à isoler la création d'objets de leur utilisation et à créer des familles d'objets liés sans dépendre de leurs classes concrètes. Cela permet d' introduire de nouveaux types dérivés sans modifier le code qui utilise la classe de base .
Le modèle décrit comment résoudre ces problèmes :
- Encapsulez la création d'objets dans un objet (usine) distinct en définissant et en implémentant une interface pour la création d'objets.
- Déléguez la création d’objets à un objet d’usine au lieu de créer des objets directement.
Cela rend une classe indépendante de la manière dont ses objets sont créés. Une classe peut être configurée avec un objet factory, qu'elle utilise pour créer des objets, et l'objet factory peut être échangé au moment de l'exécution.
Définition
Design Patterns décrit le modèle de fabrique abstraite comme « une interface permettant de créer des familles d'objets liés ou dépendants sans spécifier leurs classes concrètes. »
Usage
La fabrique détermine le type concret de l'objet à créer, et c'est à ce moment-là que l'objet est réellement créé. Cependant, la fabrique ne renvoie qu'une référence (en Java, par exemple, par l' opérateur new ) ou un pointeur d'un type abstrait vers l'objet concret créé.
Cela isole le code client de la création d'objet en demandant aux clients de demander à un objet d'usine de créer un objet du type abstrait souhaité et de renvoyer un pointeur abstrait vers l'objet.
Un exemple est une classe de fabrique abstraite DocumentCreatorqui fournit des interfaces pour créer un certain nombre de produits (par exemple, createLetter()et createResume()). Le système aurait un nombre quelconque de versions concrètes dérivées de la DocumentCreatorclasse telles que FancyDocumentCreatorou ModernDocumentCreator, chacune avec une implémentation différente de createLetter()et createResume()qui créerait des objets correspondants tels que FancyLetterou ModernResume. Chacun de ces produits est dérivé d'une classe abstraite simple telle que Letterou Resumedont le client a connaissance. Le code client acquerrait une instance appropriée de DocumentCreatoret appellerait ses méthodes de fabrique . Chacun des objets résultants serait créé à partir de la même DocumentCreatorimplémentation et partagerait un thème commun. Le client aurait seulement besoin de savoir comment gérer la classe abstraite Letterou Resume, pas la version spécifique créée par la fabrique concrète.
Comme la fabrique ne renvoie qu'une référence ou un pointeur vers un type abstrait, le code client qui a demandé l'objet à la fabrique n'a pas connaissance du type concret réel de l'objet qui a été créé et n'est pas gêné par celui-ci. Cependant, la fabrique abstraite connaît le type d'un objet concret (et donc une fabrique concrète). Par exemple, la fabrique peut lire le type de l'objet à partir d'un fichier de configuration. Le client n'a pas besoin de spécifier le type, car le type a déjà été spécifié dans le fichier de configuration. En particulier, cela signifie :
- Le code client n'a aucune connaissance du type concret , il n'a donc pas besoin d'inclure de fichiers d'en-tête ou de déclarations de classe qui lui sont liés. Le code client ne traite que du type abstrait. Les objets d'un type concret sont en effet créés par la fabrique, mais le code client n'accède à ces objets que par leurs interfaces abstraites .
- L'ajout de nouveaux types concrets s'effectue en modifiant le code client pour utiliser une fabrique différente, une modification qui se fait généralement sur une ligne dans un fichier. La fabrique différente crée alors des objets d'un type concret différent mais renvoie toujours un pointeur du même type abstrait qu'auparavant, isolant ainsi le code client de tout changement. C'est beaucoup plus simple que de modifier le code client pour instancier un nouveau type. Cela nécessiterait de modifier chaque emplacement dans le code où un nouvel objet est créé ainsi que de s'assurer que tous ces emplacements de code ont connaissance du nouveau type concret, par exemple, en incluant un fichier d'en-tête de classe concrète. Si tous les objets de fabrique sont stockés globalement dans un objet singleton , et que tout le code client passe par le singleton pour accéder à la fabrique appropriée pour la création d'objet, alors changer de fabrique est aussi simple que de changer l'objet singleton.
Structure
Diagramme UML
![Un exemple de diagramme de classe et de séquence UML pour le modèle de conception d'usine abstraite. [7]](https://images.worldlex.wiki/wikipedia/commons/a/aa/W3sDesign_Abstract_Factory_Design_Pattern_UML.jpg)
Dans le diagramme de classes UML ci-dessus , la classe qui requiert les objets et n'instancie pas directement les classes et . Au lieu de cela, la fait référence à l' interface de création d'objets, ce qui rend la classe indépendante de la manière dont les objets sont créés (quelles classes concrètes sont instanciées). La classe implémente l' interface en instanciant les classes et . ClientProductAProductBProductA1ProductB1ClientAbstractFactoryClientFactory1AbstractFactoryProductA1ProductB1
Le diagramme de séquence UML montre les interactions d'exécution. L' objet appelle l' objet, qui crée et renvoie un objet. Ensuite, l' appelle , qui crée et renvoie un objet. ClientcreateProductA()Factory1ProductA1ClientcreateProductB()Factory1ProductB1
Variantes
La structure originale du modèle de fabrique abstraite, telle que définie en 1994 dans Design Patterns , est basée sur des classes abstraites pour la fabrique abstraite et les produits abstraits à créer. Les fabriques et produits concrets sont des classes qui spécialisent les classes abstraites à l'aide de l'héritage.
Une structure plus récente du modèle est basée sur des interfaces qui définissent la fabrique abstraite et les produits abstraits à créer. Cette conception utilise un support natif pour les interfaces ou les protocoles dans les langages de programmation courants pour éviter l'héritage. Dans ce cas, les fabriques et les produits concrets sont des classes qui réalisent l'interface en l'implémentant.
Exemple
Cette implémentation C++11 est basée sur l'implémentation pré-C++98 du livre.
#include <iostream> enum Direction { Nord , Sud , Est , Ouest }; classe MapSite { public : virtuel void enter () = 0 ; virtuel ~ MapSite () = par défaut ; }; classe Salle : public MapSite { public : Salle () : Numéro de salle ( 0 ) {} Salle ( int n ) : Numéro de salle ( n ) {} void setSide ( Direction d , MapSite * ms ) { std :: cout << "Room::setSide " << d << ' ' << ms << ' ' ; } virtuel void enter () {} Salle ( const Salle & ) = supprimer ; // règle de trois Salle & opérateur = ( const Salle & ) = supprimer ; privé : int Numéro de salle ; }; classe Wall : public MapSite { public : Wall () {} virtuel void enter () {} }; classe Porte : public MapSite { public : Porte ( Salle * r1 = nullptr , Salle * r2 = nullptr ) : salle1 ( r1 ), salle2 ( r2 ) {} void virtuel enter () {} Porte ( const Porte & ) = delete ; // règle de trois Porte & opérateur = ( const Porte & ) = delete ; privé : Salle * salle1 ; Salle * salle2 ; }; classe Maze { public : void addRoom ( Room * r ) { std :: cout << "Maze::addRoom " << r << ' ' ; } Room * roomNo ( int ) const { return nullptr ; } }; classe MazeFactory { public : MazeFactory () = default ; virtuel ~ MazeFactory () = default ; Labyrinthe virtuel * makeMaze () const { renvoie un nouveau labyrinthe ; } Mur virtuel * makeWall () const { renvoie un nouveau mur ; } Pièce virtuelle * makeRoom ( int n ) const { renvoie une nouvelle pièce ( n ); } Porte virtuelle * makeDoor ( Pièce * r1 , Pièce * r2 ) const { renvoie une nouvelle porte ( r1 , r2 ); } }; // Si createMaze reçoit un objet comme paramètre à utiliser pour créer des pièces, des murs et des portes, vous pouvez alors modifier les classes des pièces, des murs et des portes en passant un paramètre différent. Ceci est un exemple du modèle Abstract Factory (99). classe MazeGame { public : Maze * createMaze ( MazeFactory & factory ) { Labyrinthe * aMaze = factory . makeMaze (); Pièce * r1 = factory . makeRoom ( 1 ); Pièce * r2 = factory . makeRoom ( 2 ); Porte * aDoor = factory . makeDoor ( r1 , r2 ); aMaze -> addRoom ( r1 ); aMaze -> addRoom ( r2 ); r1 -> setSide ( Nord , factory . makeWall ()); r1 -> setSide ( Est , aDoor ); r1 -> setSide ( Sud , factory . makeWall ()); r1 -> setSide ( Ouest , factory . makeWall ()); r2 -> setSide ( Nord , factory . makeWall ()); r2 -> setSide ( Est , factory . makeWall ()); r2 -> setSide ( Sud , usine . makeWall () ); r2 -> setSide ( Ouest , aDoor ); retourner un labyrinthe ; } } ; int main () { MazeGame jeu ; MazeFactory usine ; jeu . createMaze ( usine ); }
La sortie du programme est :
Labyrinthe :: addRoom 0x1317ed0 Labyrinthe :: addRoom 0x1317ef0 Pièce :: setSide 0 0x1318340 Pièce :: setSide 2 0x1317f10 Pièce :: setSide 1 0x1318360 Pièce :: setSide 3 0x1318380 Pièce :: setSide 0 0 Pièce :: setSide 2 0x13183c0 Pièce :: setSide 1 0x13183e0 Chambre :: setSide 3 0x1317f10