Modèle de création : usine abstraite

Dec 04 2022
Voici un article qui détaille un design pattern créationnel qui nous permet de créer une famille de produits de manière flexible et extensible. Qu'est-ce que c'est? Dans le dernier article, nous avons découvert le modèle de méthode d'usine.

Voici un article qui détaille un design pattern créationnel qui nous permet de créer une famille de produits de manière flexible et extensible.

Qu'est-ce que c'est?

Dans le dernier article, nous avons découvert le modèle de méthode d'usine . Ensuite, nous avons vu comment nous pouvions modéliser les variantes du F-16 en utilisant la méthode d'usine. Mais cet article couvrira comment représenter de nombreux avions autres que le F-16. Par exemple, supposons qu'un client achète un Boeing 747 pour que son PDG puisse voyager et souhaite que votre logiciel prenne en charge ce nouveau type d'avion.

Le modèle d'usine abstraite résout le problème de la création de familles de produits connexes. Par exemple, le F-16 a besoin d'un moteur, d'un cockpit et d'ailes. Le Boeing-747 nécessiterait les mêmes pièces, mais elles seraient spécifiques à Boeing. Tout avion aurait besoin de ces trois parties liées ; cependant, les pièces seront spécifiques à l'avion. Pouvez-vous voir un modèle émerger ici? Tout d'abord, nous avons besoin d'un cadre pour créer les pièces associées pour chaque avion.

Le modèle de fabrique abstraite est un modèle de conception qui vous permet de créer des familles d'objets liés ou dépendants sans spécifier leurs classes concrètes.

Diagramme de classe

Le diagramme de classes comprend les entités suivantes :

  • Usine abstraite
  • Usine de béton
  • Produit abstrait
  • Produit en béton
  • Client

Le modèle de fabrique abstraite vous permet de créer une famille d'objets associés sans spécifier leurs classes concrètes. Prenons un exemple. Supposons que vous créez un logiciel de simulation pour l'industrie aéronautique et que vous devez représenter différents aéronefs sous forme d'objets. Mais avant de représenter un avion, vous devez également définir les autres pièces d'un avion en tant qu'objets. Choisissons-en trois : les ailes, le cockpit et le moteur. Supposons maintenant que le premier avion que vous souhaitez représenter soit le puissant F-16. Vous écrirez probablement trois classes, une pour chaque pièce spécifique au F-16. Dans votre code, vous consommerez probablement ces trois classes que vous venez de créer, comme ceci :

Cet extrait de code semble inoffensif mais peut causer de graves maux de tête si votre logiciel de simulation décolle et que vous devez l'étendre à d'autres avions. Voici quelques-uns des problèmes avec le code ci-dessus :

  • Les classes concrètes pour les trois parties ont été exposées directement au consommateur.
  • Le F-16 a plusieurs variantes avec différents moteurs. Si vous souhaitez renvoyer un objet moteur correspondant à la variante, vous devrez sous-classer la classe F-16Engine, ce qui nécessiterait également de modifier l'extrait de code consommateur.
  • La liste dans l'extrait de code est déclarée paramétrée avec une classe concrète ; si vous ajoutez un autre moteur d'avion à votre programme, il ne sera pas reconnu par la liste, même si les moteurs de tous les avions sont quelque peu similaires.

Traiter une interface comme un concept abstrait plutôt qu'une implémentation

Une bonne conception orientée objet signifie masquer les classes concrètes utilisées dans votre application et exposer les interfaces aux clients. Un objet répond à un ensemble de requêtes qui peuvent être capturées par une interface implémentée par la classe de l'objet. Les clients doivent savoir à quelles demandes un objet répond plutôt que l'implémentation.

Dans notre exemple, nous allons créer une interface qui expose une méthode appelée start() . La classe F16Engine changera alors comme suit :

Voyez comment le code consommateur correspondant change avec le changement ci-dessus.

Le code consommateur est désormais exempt des détails d'implémentation du fonctionnement du moteur F-16 et de la classe qui l'implémente. Cependant, nous ne voulons toujours pas révéler la partie F16Engine() du code. Nous voulons que nos consommateurs continuent de deviner quelle classe nous instancions. Ceci est discuté ensuite.

Créer une usine

Au lieu de créer des objets dans le code client, nous aurons une classe de fabrique qui crée les objets demandés et les renvoie au client. Nous appellerons cette classe F16Factory car elle peut produire diverses pièces de l'avion militaire F16 et les livrer au client demandeur. La classe prendrait la forme suivante.

Supposons maintenant que nous transmettions l' objet F16Factory au code client en tant qu'argument constructeur, et qu'il puisse alors créer des objets comme ceci :

Notez comment cette configuration nous permet de changer la classe concrète représentant le F16Engine tant qu'il s'engage dans l' interface IEngine . Nous pouvons renommer, améliorer ou modifier notre classe sans provoquer de changement radical dans le client. Notez également qu'en changeant uniquement la classe d'usine transmise au constructeur client, nous pouvons fournir au client différentes pièces pour un avion entièrement nouveau.

Usine d'usines : Grande usine qui fabrique d'autres usines

Ne serait-il pas formidable d'utiliser le même extrait client pour différents types d'avions, comme un Boeing 747 ou un MiG-29 russe ? Si toutes les usines implémentant createEngine() s'accordent sur une interface standard, notre code client continuera à fonctionner pour toutes les usines aéronautiques. Mais toutes les fabriques devraient s'engager sur cette interface commune dont elles implémenteront les méthodes, et cette interface familière est une fabrique abstraite .

Mise en œuvre

Commençons par une interface définissant les méthodes que les différentes usines aéronautiques doivent mettre en place. Le code client est écrit sur cette interface mais sera composé au moment de l'exécution avec une usine concrète.

Lorsque nous nous référons à « interface », nous entendons une interface Java ou une classe abstraite. Dans ce cas, nous aurions pu utiliser une classe abstraite s'il y avait des implémentations par défaut pour l'un des produits. Les méthodes de création ne renvoient pas de produits concrets - elles renvoient des interfaces pour dissocier les consommateurs d'usine de l'implémentation concrète des pièces.

La définition formelle du modèle de fabrique abstraite indique qu'il s'agit d'un modèle de conception qui définit une interface pour créer des familles de produits connexes sans spécifier les classes concrètes. Dans ce cas, IAircraftFactory est cette interface, et ses méthodes de création ne renvoient pas de parties substantielles mais des interfaces implémentées par les classes des parties concrètes.

Ensuite, installons les usines pour nos deux avions

Une usine de béton produira un moteur, un cockpit et des ailes spécifiques au F-16 ou au Boeing. Chaque partie a une interface de produit correspondante que nous ne listons pas ici par souci de brièveté. Les interfaces produit représentant les pièces seraient :

  • IEngine
  • ICockpit
  • IWings

Dans l'article précédent , nous avons créé une classe pour F-16 qui incluait une méthode fly() . Cette méthode a appelé makeF16() , qui a construit une instance de F-16, puis a appelé taxi() . Une fois sur la piste, il s'est ensuite envolé. Nous pouvons appliquer cette même approche à notre exemple Boeing-747. Tous les avions suivent le même schéma : se faire fabriquer, rouler sur la piste, puis s'envoler. Nous pouvons ainsi créer une classe pour un avion qui effectue ces trois tâches. Notez que nous ne créons pas de classes distinctes pour représenter les deux avions (c'est-à-dire F-16 et Boeing-747) mais plutôt une seule classe d'avion qui peut représenter les deux.

Pour le moment, nous garderons la méthode makeAircraft vide. Voyons donc d'abord comment un client demandera des objets F-16 et Boeing-747.

Pour ajouter un constructeur à notre classe Aircraft , nous devrons créer une nouvelle variable d'instance appelée factory. L'usine stocke l'objet qui sera chargé de créer des pièces d'avion. Nous pouvons obtenir un avion différent en composant l'objet avion avec une autre usine. La version complète de la classe Aircraft est ci-dessous :

Le client doit instancier la bonne usine, qu'il passe à la classe Aircraft . La classe Aircraft représente le client. Nous aurions pu créer une interface IAircraft pour représenter tous les avions que notre usine peut créer, mais pour notre exemple limité, c'est inutile.

Le code résultant est facilement étendu et modifié.

Pour continuer notre exemple de l'article sur le modèle de méthode d'usine , nous pouvons utiliser soit la sous-classe de la classe F16Factory pour créer des usines pour les variantes A et B de F-16, soit la paramétrer avec une énumération spécifiant le modèle de variante et renvoyer la bonne partie dans un commutateur déclaration.

Autres exemples

  • Le modèle d'usine abstrait aide à créer des familles de produits connexes. Par exemple, si votre bibliothèque fournit des widgets d'interface utilisateur sophistiqués et prend en charge macOS et Windows, vous aurez peut-être besoin d'une famille de produits fonctionnant sur ces plates-formes. De même, les thèmes utilisés dans les IDE peuvent être un autre exemple. Par exemple, si votre IDE prend en charge les thèmes clairs et sombres, il peut utiliser le modèle de fabrique abstraite pour créer des widgets appartenant au thème clair ou sombre en faisant varier la fabrique concrète qui fabrique les widgets.
  • javax.xml.parsers.DocumentBuilderFactory.newInstance()vous renverra une usine.
  • javax.xml.transform.TransformerFactory.newInstance()vous renverra une usine.
  • La méthode de fabrique et le modèle de fabrique abstraite sont similaires en ce sens qu'ils aident à gérer la création d'objets. La différence entre les deux modèles réside dans leurs motivations. Le modèle de méthode d'usine est généralement responsable de la création d'un seul produit, tandis qu'un modèle d'usine abstrait crée des familles entières de produits connexes. De plus, nous utilisons l'héritage dans le modèle de méthode d'usine pour construire des produits plus spécialisés. Enfin, nous pratiquons la composition d'objets dans un modèle d'usine abstrait en passant dans des usines consommées pour créer les résultats souhaités.
  • Nous créons une usine à béton lorsque nous ajoutons un nouvel avion à notre flotte. Mais supposons qu'un hélicoptère soit ajouté à la flotte et nécessite une pièce qu'un avion n'a pas. Dans ce cas, nous devrons étendre l'interface IAircraftFactory avec une autre méthode créative pour la pièce requise uniquement par l'hélicoptère. Cela entraînera la modification en cascade des usines existantes devant renvoyer null puisque le nouveau composant ne fait pas partie des jets.
  • Les usines en béton sont mieux implémentées en tant qu'objets singleton.