Motifs de Création : Fabrique Abstraite

décembre4, 2007

Deuxième article dans la série des Design Patterns (ou Motifs de Conception), celui-ci nous expliquera le rôle et l’utilisation du motif Fabrique Abstraite (Abstract Factory)

But

Fournir une interface pour la création de famille d’objets connexes ou dépendants sans indiquer leur classe concrète.

Autre Nom

Abstract Factory, Kit.

Exemple

Prenons un gestionnaire de composants d’interface utilisateur supportant un certain nombre de styles d’affichage, par exemple un style bleu et un style gris (comme les styles XP). Différents styles définissent différentes apparences et comportements pour les composants d’interface utilisateur tels que les barres de défilement, les fenêtres, et les boutons. Pour être portable sur les différents styles, une application ne devrait pas coder en dur ses composants pour un style particulier. L’instanciation de classes de composants spécifiques à un style au travers de l’application rendrait difficile la modification du style par la suite.

On peut résoudre ce problème en définissant une classe abstraite FabriqueDeComposants déclarant une interface pour créer chaque type basique de composant. On crée aussi une classe abstraite pour chaque type de composant, et les sous-classes concrètes qui vont implémenter chaque composant pour un style spécifique. L’interface de FabriqueDeComposants a une méthode qui renvoie un nouvel objet Composant pour chaque classe abstraite de composant. Les applications clientes font appel à ces méthodes pour obtenir des instances de composants, mais elles n’ont pas conscience de la classe concrète qu’elles utilisent. Par conséquent les applications clientes restent indépendantes du style sélectionné.

Schéma d’exemple - Fabrique Abstraite

Il y a une sous-classe concrète de FabriqueDeComposants pour chaque style. Chaque sous-classe implémente les opérations pour créer les composants appropriés au style. Par exemple, la méthode CreerScrollBar de FabriqueDeComposantsBleus instancie et retourne une barre de défilement bleue, tandis que la même méthode dans la classe FabriqueDeComposantsGris renvoie une barre de défilement grise. Les applications clientes créent des composants uniquement via l’interface FabriqueDeComposants et n’ont pas connaissance des classes implémentant les composants pour un style donné. En d’autres termes, les applications clientes doivent seulement appliquer une interface définie par une classe abstraite, et non par une classe concrète particulière.

Une FabriqueDeComposants renforce également la cohésion entre les classes concrètes de composants. Une barre de défilement bleue doit être utilisée avec un bouton bleu dans une fenêtre bleue, et cette contrainte est automatiquement appliquée par l’utilisation de FabriqueDeComposantsBleus.

Applicabilité

Utilisez le motif Fabrique Abstraite quand :

  • Un système doit être indépendant de la façon dont ses produits sont créés, composés et représentés.
  • Un système doit être configurée avec l’une des multiples familles de produits.
  • Des objets relatifs les uns aux autres sont fait pour être utilisés ensemble, et vous voulez renforcer cette contrainte.
  • Vous voulez fournir une bibliothèque de classes de produits, et vous voulez juste donner leur interface, et non leurs implémentations.

Structure

Schéma de Structure - Fabrique Abstraite

Participants

  • FabriqueAbstraite (FabriqueDeComposants) :
    • Déclare une interface de création de ProduitAbstrait.
  • FabriqueConcrete (FabriqueDeComposantsBleus, FabriqueDeComposantsGris) :
    • Implémente les méthodes de création de ProduitConcret.
  • ProduitAbstrait (ScrollBar, Fenetre, Bouton) :
    • Déclare une interface pour un type de produit.
  • ProduitConcret (ScrollBarBleus, FenetreGris) :
    • Définit un produit devant être créé par la FabriqueConcrete correspondante.
    • Implémente l’interface ProduitAbstrait.
  • Client :
    • Utilise uniquement les interfaces déclarées par les classes FabriqueAbstraite et ProduitAbstrait.

Collaboration

  • Normalement, une seule instance d’une classe FabriqueConcrete est crée au moment de l’exécution. Cette FabriqueConcrete va créer des objets ayant une implémentation particulière. Pour créer des objets d’une implémentation différente, l’application Client doit utiliser une autre FabriqueConcrete.
  • FabriqueAbstraite défère la création d’objet à ses sous-classes FabriqueConcrete.

Conséquences

Le motif Fabrique Abstraite génère les avantages et les inconvénients suivants :

  1. Il isole les classes concrètes. Le motif Fabrique Abstraite vous aide à contrôler les classes d’objets qu’une application peut créer. Comme une fabrique encapsule la responsabilité et le processus de création des objets, il isole le client des classes d’implémentation. Le client va manipuler les instances au travers de leurs interfaces abstraites. Les noms des classes de produits concrets sont isolés dans l’implémentation des fabriques concrètes ; ils n’apparaissent pas dans le code client.
  2. Il permet d’échanger facilement des familles de produit. La classe d’une fabrique concrète apparaît une seule fois dans l’application : là où elle est instanciée. Cela rend très simple le changement de fabrique utilisé par l’application. Le client peut utiliser différentes configurations de produits en modifiant simplement la fabrique concrète. COmme une fabrique abstraite crée une famille complète de produits, la famille entière de produit change en une seule opération. Dans notre example d’interface utilisateur, on peut passer des composants bleus aux composants gris en changeant simplement de fabrique de composant.
  3. Il garantit la cohérence entre les produits. Quand les objets d’une famille sont fait pour travailler ensemble, il est important qu’une application n’utilise que les objets d’une seule famille à la fois. Fabrique Abstraite met facilement cela en oeuvre.
  4. Il est difficile de prendre en charge de nouveaux types de produits. Etendre Fabrique Abstraite pour qu’elle supporte de nouveaux produits n’est pas facile. C’est parce que l’interface FabriqueAbstraite fixe l’ensemble de produits pouvant être créés. La prise en charge de nouveaux produits demande l’extension de l’interface de fabrication, ce qui implique de modifier la classe FabriqueAbstraite et toutes ses sous-classes. Nous discuterons d’une solution à ce problème dans la section suivante.

Implémentation

Quelques techniques utiles pour implémenter le motif Fabrique Abstraite :

  1. Développer les fabriques en tant que Singleton. Une application à besoin, typiquement, d’une seule instance d’une FabriqueConcrete par famille de produit. Il est donc généralement préférable de les implémenter en tant que Singleton.
  2. Créer les produits. FabriqueAbstraite déclare seulement une interface pour créer les produits. C’est aux sous-classes ProduitsConcrets de les créer. La méthode la plus utilisée de faire ceci est de définir une méthode de fabrication (voir Fabrique) pour chaque produit. Une fabrique concrète va spécifier l’implémentation de ses produits en surchargeant la méthode de fabrication pour chacun. Mais si cette implémentation est simple, elle demande une nouvelle classe de fabrique concrète pour chaque famille de produit, même si ces familles diffèrent très peu.
  3. Si l’on risque d’avoir beaucoup de familles de produits, la fabrique concrète peut être implémentée en utilisant le motif Prototype. La fabrique concrète est initialisée avec une instance prototypique de chaque produit de la famille, et crée un nouveau produit en copiant ce prototype. Cette approche basée sur Prototype élimine le besoin d’une nouvelle classe de fabrique concrète pour chaque famille de produit.

    Voici une manière d’implémenter une fabrique basée sur Prototype en SmallTalk. La fabrique concrète stocke les prototypes à cloner dans un dictionnaire appelé partCatalog. La méthode make: retrouve le prototype et le clone :

    make: partName
           ^ (partCatalog at: partName) copy

    La fabrique concrète a une méthode pour ajouter une pièce au catalogue :

    addPart: partTemplate named: partName   partCatalog at: partName put: partTemplate

    Les Prototypes sont ajoutés à la fabrique avec un identifiant :

    aFactory addPart: aPrototype named: #ACMEWidget
  4. Une variation de l’approche basée sur Prototype est possible dans les langages traitant les classes comme des objets de premier niveau (les langages orientés objets comme .Net, Java, C++, etc…). On peut voir une classe dans ces langages comme une fabrique dégénérée qui ne créerait qu’un seul type d’objet. On peut enregistrer ces classes dans une fabrique concrète qui crée les nombreux produits concrets en tant que variables, comme des prototypes. Ces classes vont créer de nouvelles instances à la place de la fabrique concrète. On définit une nouvelle fabrique en initialisant une instance de fabrique concrète contenant des classes de produits au lieu de sous-classer. Cette approche met à profit les caractéristiques de langage, au lieu de l’approche exactement basée sur Prototype, qui est indépendante du langage.Comme la fabrique basée sur Prototype dont on vient de parler en SmallTalk, la version basée sur les classes n’aura qu’une seule variable d’instance CatalogueDePièce, qui est un dictionnaire dont les clés sont les noms des pièces. Au lieu de conserver des prototypes à cloner, CatalogueDePièce enregistre les classes des produits. La méthode Créer ressemble maintenant à ça :
    make: partName
           ^ (partCatalog at: partName) new
  5. Définir des fabriques extensibles. Fabrique Abstraite définit généralement une méthode différente pour chaque type de produit qu’elle peut fabriquer. Les types de produits sont encodés dans les signatures de ces méthodes. Ajouter un nouveau type de produit demande la modification de l’interface de FabriqueAbstraite et de toutes ses sous-classes.Une manière de faire, plus flexible mais moins sûre, est d’ajouter un paramètre aux méthodes de création d’objets. Ce paramètre spécifie le type d’objet à créer. Cela peut être un identifiant de classe, un entier, une chaine de caractères, ou n’importe quoi d’autre qui identifie le type de produit. Dans les faits, avec cette approche, FabriqueAbstraite n’a besoin que d’une méthode Creer avec un paramètre indiquant le type d’objet à créer. C’est la technique utilisée dans la fabrique abstraite basée sur Prototype évoquée ci-dessus.Cette variation est plus simple à utiliser dans un langage typé dynamiquement comme SmallTalk plutôt que dans un langage typé statiquement comme C++. Vous pouvez l’utiliser en C++ uniquement quand tous les objet ont la même classe abstraite de base, ou bien quand les produits peuvent être transtypés de manière sûre par le client qui les a demandés. La section Implémentation de la Fabrique montre comment implémenter de telles méthodes paramétrées en C++.Mais même quand le transtypage n’est pas requis, il reste toujours un problème : tous les produits sont retournés au client avec la même interface abstraite que le type de retour. Si le client a besoin d’appeler une méthode spécifique à la sous-classe, elle ne sera pas accessible au travers de cette interface abstraite. Même si le client peut effectuer une conversion (i.e. avec dynamic_cast, en C++), ce n’est pas toujours faisable ou sûr, parce que la conversion peut échouer. C’est le compromis classique d’une interface hautement flexible et extensible.

Exemple de code

On va appliquer le motif Fabrique Abstraite à la création de labyrinthes discutée au début de ce chapitre.

La classe FabriqueDeLabyrinthe peut créer des composants de labyrinthes. Elle construit des pièces, des murs, et des portes entre les pièces. Elle peut être utilisée par un programme qui lit des plans de labyrinthe depuis un fichier, et construit le labyrinthe correspondant. Ou elle peut être utilisée par un programme qui construit des labyrinthes de manière aléatoire. Ces programmes construisant des labyrinthes prennent une FabriqueDeLabyrinthe en argument de manière à ce que le programmeur puisse spécifier les classes de pièces, de murs et de portes à construire.

Class FabriqueDeLabyrinthe
    Public Sub New()
    End Sub
 
    Public Shared Function CreerLabyrinthe() As Labyrinthe
        Return New Labyrinthe
    End Function
 
    Public Shared Function CreerMur() As Mur
        Return New Mur
    End Function
 
    Public Shared Function CreerPiece(ByVal n As Integer) As Pièce
        Return New Pièce(n)
    End Function
 
    Public Shared Function CreerPorte(ByVal p1 As Pièce, ByVal p2 As Pièce) As Porte
        Return New Porte(p1, p2)
    End Function
 
End Class

Souvenez-vous de la fonction NouveauLabyrinthe qui construit un petit labyrinthe de deux pièces séparées par une porte. NouveauLabyrinthe code « en dur » les noms des classes, ce qui rend difficile la création de labyrinthes avec d’autres composants.

Voici une version de NouveauLabyrinthe qui va remédier à ce défaut en prenant une FabriqueDeLabyrinthe en paramètre :

Public Function NouveauLabyrinthe(fabrique as FabriqueDeLabyrinthe) as Labyrinthe
    Dim oLabyrinthe as Labyrinthe = fabrique.CreerLabyrinthe()
 
    Dim oPiece1 = fabrique.Pièce(1)
    Dim oPiece2 = fabrique.Pièce(2)
    Dim oPorte = fabrique.Porte(oPiece1, oPiece2)    oLabyrinthe.AjouterPiece(oPiece1)
    oLabyrinthe.AjouterPiece(oPiece2)
 
    oPiece1.Cote(Nord) = fabrique.Mur()
    oPiece1.Cote(Sud) = fabrique.Mur()
    oPiece1.Cote(Est) = oPorte
    oPiece1.Cote(Ouest) = fabrique.Mur()
 
    oPiece2.Cote(Nord) = fabrique.Mur()
    oPiece2.Cote(Sud) = fabrique.Mur()
    oPiece2.Cote(Est) = fabrique.Mur()
    oPiece2.Cote(Ouest) = oPorte
 
    Return oLabyrinthe
 
End Class

On peut créer une FabriqueDeLabyrintheEnsorcellé en sous-classant FabriqueDeLabyrinthe. FabriqueDeLabyrintheEnsorcellé va surcharger différentes fonctions et retourner différentes sous-classes de Pièce, Mur, etc.

Class FabriqueDeLabyrintheEnsorcellé
    Inherits FabriqueDeLabyrinthe
 
    Private Function JetteUnSort() As Boolean
        Dim oRnd As Random = New Random(CInt(DateTime.Now.Ticks))
 
        If oRnd.Next(0, 5) = 0 Then
            Return True
        Else
            Return False
        End If
 
    End Function
 
    Public Shared Function CreerPiece(ByVal n As Integer) As Pièce
        Return New PièceEnsorcellée(n, JetteUnSort())
    End Function
 
    Public Shared Function CreerPorte(ByVal p1 As Pièce, ByVal p2 As Pièce) As Porte
        Return New PorteAvecSort(p1, p2)
    End Function
 
End Class

Maintenant, supposons que l’on veuille faire un jeu de labyrinthe dans lequel les pièces peuvent contenir une bombe. Si la bombe explose, elle va endommager les murs. On peut créer une sous-classe de Pièce qui garde en mémoire si la pièce contient une bombe et si celle-ci a explosé ou non. Nous aurons également besoin d’une sous-classe Mur qui gardera une trace des dommages causés. Nous appellerons ces classes PiècePiégée et MurExplosé.

La dernière classa que nous définirons sera FabriqueDeLabyrinthePiégé, une sous-classe de FabriqueDeLabyrinthe qui s’assure que les murs sont de la classe MurExplosé et que les pièces sont de la classe PiècePiégée. FabriqueDeLabyrinthePiégée n’a besoin de surcharger que deux fonctions :

Class FabriqueDeLabyrintheEnsorcellé
    Inherits FabriqueDeLabyrinthe
 
    Public Shared Function CreerPiece(ByVal n As Integer) As Pièce
        Return New PiècePiégée(n)
    End Function
 
    Public Shared Function CreerMur() As Mur
        Return New MurExplosé()
    End Function
 
End Class

Pour construire un labyrinthe simple contenant des bombes, on va simplement appeler NouveauLabyrinthe avec une FabriqueDeLabyrinthePiégé.

Dim jeu as JeuDuLabyrinthe
Dim fabrique as FabriqueDeLabyrinthePiégé
 
jeu.NouveauLabyrinthe(fabrique)

NouveauLabyrinthe peut aussi bien recevoir une instance de FabriqueDeLabyrintheEnsorcellé afin de construire des labyrinthes ensorcellés.

Notez bien que la FabriqueDeLabyrinthe est juste une collection de éthodes de fabrication. C’est la manière la plus courante d’implémenter le motif Fabrique Abstraite. Notez également que FabriqueDeLabyrinthe n’est pas une classe abstraite ; même si elle agit aussi bien en tant que FabriqueAbstraite au’en tant que FabriqueConcrète. C’est une autre implémentation courante du motif Fabrique Abstraite. Comme la FabriqueDeLabyrinthe est une classe concrète consistant entièrement en des méthodes de fabrication, il est très simple de faire une nouvelle FabriqueDeLabyrinthe en créant une sous-classe et en surchargeant uniquement les méthodes qui en ont besoin.

NouveauLabyrinthe utilise la propriété Cote des pièces pour spécifier leurs côtés. Si cette méthode crée des pièces avec une FabriqueDeLabyrinthePiégé, alors le labyrinthe sera fait d’objets PiècePiégée avec des objets MurExplosé sur les côtés. Si PiècePiégée doit accéder à un membre spécifique à la sous-classe MurExplosé, alors elle devra convertir les références à ses murs de Mur à MurExplosé. Ce sous-classement est sûr tant que les murs sont bien des MurExplosé, ce qui est garanti si les murs sont construit grâce à FabriqueDeLabyrinthePiégé.

Les langages typés dynamiquement comme SmallTalk ne requièrent pas de sous-classement bien sûr, mais ils pourraient produire des erreurs de runtime s’ils rencontrent un Mur alors qu’ils attendaient une sous-classe de Mur. L’utilisation de Fabrique Abstraite aide à la prévention de ces erreurs en s’assurant que seuls certains types de murs peuvent être créés.

Considérons une version SmallTalk de FabriqueDeLabyrinthe, une version avec juste une opération de création prenant le type d’objet à créer en paramètre. En outre, la fabrique concrète garde enregistre les classes de produits qu’elle crée.

Tout d’abord, on écrit un équivalent de NouveauLabyrinthe en SmallTalk :

createMaze: aFactory  | room1 room2 aDoor |
 
room1 := (aFactory make: #room) number: 1.
 
room2 := (aFactory make: #room) number: 2.
 
aDoor := (aFactory make: #door) from: room1 to: room2.
 
room1 atSide: #north put: (aFactory make: #wall).
 
room1 atSide: #east put: aDoor.
 
room1 atSide: #south put: (aFactory make: #wall).
 
room1 atSide: #west put: (aFactory make: #wall).
 
room2 atSide: #north put: (aFactory make: #wall).
 
room2 atSide: #east put: (aFactory make: #wall).
 
room2 atSide: #south put: (aFactory make: #wall).
 
room2 atSide: #west put: aDoor.
 
^ Maze new addRoom: room1; addRoom: room2; yourself

Comme on l’a vu dans la section Implémentation, FabriqueDeLabyrinthe à seulement besoin d’une unique variable d’instance de CatalogueDePièce pour fournir un dictionnaire dont la clé est la classe de composant. Ainsi, voyons comment nous allons implémenter la méthode Construire :

make: partName
  ^ (partCatalog at: partName) new

Maintenant, on peut créer une FabriqueDeLabyrinthe et l’utiliser pour implémenter NouveauLabyrinthe. On vas créer la fabrique en utilisant une méthode NouvelleFabriqueDeLabyrinthe de la classe JeuDuLayrinthe.

createMazeFactory
  ^ (MazeFactory new
 
addPart: Wall named: #wall;
 
addPart: Room named: #room;
 
addPart: Door named: #door;
 
yourself)

Une FabriqueDeLabyrinthePiégé ou une FabriqueDeLabyrintheEnsorcelé est créée en associant différentes classes aux clés. Par exemple, une FabriqueDeLabyrintheEnsorcelé pourrait être crée comme ceci :

createMazeFactory
  ^ (MazeFactory new
 
addPart: Wall named: #wall;
 
addPart: EnchantedRoom named: #room;
 
addPart: DoorNeedingSpell named: #door;
 
yourself)

Utilisations connues

InterViews utilise le suffixe « Kit » pour les classes FabriqueAbstraite. Il définit les fabriques abstraites WidgetKit et DialogKit pour générer des objets d’interface utilisateur avec un style spécifique. InterViews inclus également un LayouKit qui génère différents objets de composition, dépendants de la mise en page désirée. Par exemple, une mise en page horizontale va demander des objets de composition différents selon l’orientation du document (porttrait ou paysage).

ET++ utilise la Fabrique Abstraite pour permettre la portabilité entre différents systèmes de fenêtrage (X Windows et SunView, par exemple). La classe abstraite de base WindowSystem définit l’interface pour créer des objets représentant les ressources d’un système de fenêtrage (MakeWindow, MakeFont, MakeColor, par exemple). Les classes concrètes dérivées implémentent les interfaces pour un système de fenêtrage spécifique. Lors de l’exécution, ET++ crée une instance d’une classe concrète dérivée de WindowSystem qui crée des objets concrets de ressources systèmes.

Voir Aussi

Les classes FabriqueAbstraite sont souvent implémentée avec le motif Fabrique, mais elles peuvent également être implémentée avec Prototype.

Une fabrique concrète est souvent un Singleton.

Vous pouvez suivre les réponses à cet article grâce à ce flux RSS 2.0.
Vous pouvez laisser un commentaire, ou faire un trackback.

Laissez un Commentaire


Vers le haut