Motifs de Création : Monteur

décembre6, 2007

Troisiè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 Monteur (Builder)

But

Séparer la construction d’un objet complexe de sa représentation, afin qu’un même processus de constrauction puisse créer différentes représentations.

Autre Nom

Builder

Exemple

Un lecteur pour le format d’échange de documents RTF (Rich Texte Format) devrait être capable de convertir le RTF vers de multiples formats textes. Le lecteur pourrait convertir des documents RTF en texte ASCII ou en un widget texte pouvant être édité interactivement. Le problème, cependant, est que le nombre de conversions possible est sans fin. Donc, on devrait pouvoir ajouter de nouvelles conversions sans modifier le lecteur.

Une solution serait de configurer la classe LecteurRTF avec un objet ConvertisseurDeTexte qui convertirait le RTF en une autre représentation textuelle. Pendant que le LecteurRTF analyse le document RTF, il utilise le ConvertisseurDeTexte pour effectuer la conversion. Chaque fois que le LecteurRTF reconnaît un motif RTF (que ce soit du texte simple ou un symbole de contrôle RTF), il fait une demande au ConvertisseurDeTexte pour convertir le motif. Les objets ConvertisseurDeTexte sont responsables tant de la conversion de données que de la représentation d’un motif dans un format particulier.

Les sous-classes de ConvertisseurDeTexte sont spécialisées dans différentes conversions vers différents formats. Par exemple, un ConvertisseurASCII ignore les demandes de conversion qui ne concernent pas du texte simple. D’un autre côté, un ConvertisseurTeX va implémenter des opérations pour toutes les requêtes afin de produire une représentation TeX qui va reproduire toutes les informations de style du texte. Un ConvertisseurWidgetTexte va produire un objet d’interface utilisateur complexe qui permettra à l’utilisateur de voir et d’éditer le texte.

Schéma d’exemple - Monteur

Chaque type de classe de convertisseur va implémenter le mécanisme de création et d’assemblage d’un objet complexe en n’exposant qu’une interface abstraite. Le convertisseur est séparé du lecteur, qui est responsable de l’analyse du document RTF.

Le motif de conception Monteur prend en compte toutes ces relations. Chaque classe de convertisseur est appelée « monteur » dans le motif, et le lecteur est appelé le « directeur ». Appliqué à cet exemple, le Monteur sépare les algorithmes pour l’interprétation d’un format texte (c’est-à-dire l’analyseur de documents RTF) de la manière dont un format de conversion est créé et représenté. Cela nous permet de réutiliser l’algorithme d’analyse du LecteurRTF afin de créer différentes représentation de documents RTF : il suffit de configurer le LecteurRTF avec différentes sous-classes de ConvertisseurDeTexte.

Applicabilité

Utilisez le motif de conception Monteur quand :

  • L’algorithme de création d’un objet complexe doit être indépendant des parties qui construisent l’objet et de comment celles-ci sont assemblées.
  • Le processus de construction doit permettre différentes représentations de l’objet construit.

Structure

Schéma de structure - Monteur

Participants

  • Monteur (ConvertisseurDeTexte)
    • Spécifie une interface abstraite afin de créer les parties d’un objet Produit.
  • MonteurConcret (ConvertisseurASCII, ConvertisseurTeX, ConvertisseurWidgetTexte)
    • Construit et assmble les pièces du produit en implémentant l’interface Monteur.
    • Définit et garde une trace de la représentation qu’il crée.
    • Fournit une interface pour retrouver le produit (i.e., GetTexteASCII, GetTexteWidget)
  • Directeur (LecteurRTF)
    • Construit un objet en utilisant l’interface Monteur.
  • Produit (TexteASCII, TexteTeX, TexteWidget)
    • Représente l’objet complexe en construction. MonteurConcret construit la représentation interne du produit et définit le processus par lequel il est assemblé.
    • Comprend les classes qui définissent les éléments constitutifs, y compris les interfaces pour l’assemblage des pièces en un résultat final.

Collaboration

  • Le client crée l’objet Directeur et le configure avec l’objet Monteur désiré.
  • Le Directeur notifie le Monteur à chaque fois qu’une partie du Produit doit être construite.
  • Le Monteur gère les demandes du Directeur et ajoute des parties au Produit.
  • Le client récupère le Produit auprès du Monteur.

Le diagramme d’interaction suivant illustre la manière dont le Monteur et le Directeur coopèrent avec le client.

Diagramme de collaboration - Monteur

Conséquences

Voici les principales conséquences du motif de conception Monteur :

  1. Il permet de varier la représentation interne d’un produit. L’objet Monteur fournit une interface abstraite au Directeur pour la construction du produit. L’interface permet au monteur de cacher la représentation et la structure interne du produit. Elle cache également la manière dont est assemblé le produit. Comme le produit est construit au travers d’une interface abstraite, tout ce que vous avez à faire pour changer la représentation interne du produit est de définir un nouveau type de monteur.
  2. Il isole le code de construction et le code de représentation. Le motif de conception Monteur améliore la modularité en encapsulant la manière dont un objet complexe est construit et représenté. Les clients n’ont pas besoin de savoir quoi que ce soit à propos de la classe qui définit la structure interne du produit ; ces classes n’apparaissent pas dans l’interface Monteur.Chaque MonteurConcret contient tout le code pour créer et assembler un certain type de produit. Le code n’est écrit qu’une fois ; puis différents Directeurs peuvent le réutiliser afin de construire des Produits dérivants d’un même ensemble de pièces. Dans l’exemple du RTF que l’on vient de voir, nous pourrions définir un lecteur pour un format autre que RTF, comme un LecteurSGML, et utiliser les même ConvertisseursDeTexte pour générer du TexteASCII, du TexteTeX ou du TexteWidget venant de documents SGML.
  3. Il permet un meilleur contrôle du processus de construction. À la différence des motifs de création qui construisent un produit d’un seul coup, le motif de conception Monteur construit le produit étape par étape sous le contrôle du Directeur. Ce n’est que lorsque le produit est fini que le Directeur récupère le produit auprès du Monteur. On peut donc dire que l’interface Monteur reflète mieux le processus de construction d’un produit que les autres motifs de création. Cela permet un meilleur contrôle du processus de construction et, par conséquent, sur la structure interne du produit résultant.

Implémentation

Typiquement, il y a une classe abstraite Monteur qui définit une opération pour chaque composant que le Directeur pourrait lui demander de créer. Par défaut, ces opérations ne font rien. Une classe MonteurConcret surcharge les opérations concernant les composants dont la création l’intéresse.

Quelques autres questions à se poser sur l’implémentation :

  1. Assemblage et construction d’interface. Les Monteurs construisent leurs produit dans un mode étape par étape. Par conséquent, l’interface de la classe Monteur doit être assez générale pour permettre la construction de produits pour tout type de MonteurConcret.L’une des principales questions de conception concerne le modèle pour le processus de construction et d’assemblage. Un modèle dans lequel les résultats des demandes de construction sont simpllement ajoutés les uns aux autres est habituellement suffisant. Dans l’exemple du RTF, le Monteur convertit et ajoute le motif suivant au texte qu’il a convertit jusqu’ici.Mais parfois, vous pourriez avoir besoin d’accéder à des parties du produit construit plus tôt. Dans l’exemple du Labyrinthe présenté au chapitre « Exemple de code », l’interface MonteurDeLabyrinthe permet d’ajouter une porte entre des Pièces existantes. Les structures arborescentes comme les arbres d’analyse vus plus bas sont un autre exemple. Dans ce cas, le Monteur va renvoyer des noeuds enfants au Directeur, qui va ensuite les retourner au Monteur pour construire les noeuds parents.
  2. Pourquoi ne pas rendre abstraites les classes de produits également ? Dans la plupart des cas, les produits construits par les MonteurConcret diffèrent tellement dans leur représentation qu’il y a très peu de gain à donner aux différents produits une classe parente commune. Dans l’exemple du RTF, les objets TexteASCII et TexteWidget sont trop différents pour avoir une interface commune, et n’en ont pas besoin. Comme le client va configurer le Directeur avec le bon MonteurConcret, le client est en position de savoir quelle sous-classe de Monteur est utilisée et peut gérer ses produits dans ce sens.
  3. Par défaut, les méthodes du Monteur sont vides. En C++, les méthodes de construction ne sont intentionnellement pas déclarées comme de pures fonctions membres virtuelles. Au lieu de cela, elles sont définies comme des méthodes vide, laissant le client surcharger uniquement celles qui l’intéressent.

Exemple de code

On va définir une variante de la fonction NouveauLabyrinthe qui va prendre un monteur de type MonteurDeLabyrinthe en argument.

La classe MonteurDeLabyrinthe définit l’interface suivante pour construire des labyrinthes :

Class MonteurDeLabyrinthe
 
    Protected Sub New()
    End Sub
 
    Public Shared Sub MonteLabyrinthe()
    End Sub
 
    Public Shared Sub MontePièce(ByVal numPièce as Integer)
    End Sub
 
    Public Shared Sub MontePorte(ByVal dePièce as Integer, ByVal àPièce as Integer)
    End Sub
 
    Public Shared Function GetLabyrinthe() as Labyrinthe
        Return Nothing
    End Function
 
End Class

Cette interface peut créer trois choses : (1) le labyrinthe, (2) des pièces avec un numéro de pièce particulier, et (3) des portes entre des pièces numérotées. La fonction GetLabyrinthe retourne un labyrinthe au client. Les sous-classes de MonteurDeLabyrinthe vont surcharger ces opérations pour renvoyer le labyrinthe qu’elles auront construit.

Toutes les opérations de construction de labyrinthe de MonteurDeLabyrinthe ne font rien par défaut. Elles ne sont pas déclarées comme purement virtuelles afin de laisser aux classes dérivées le soin de surcharger uniquement les méthodes qui les intéressent.

Etant donné l’interface de MonteurDeLabyrinthe, on peut modifier la fonction NouveauLabyrinthe afin qu’elle prenne ce Monteur en paramètre.

Public Function NouveauLabyrinthe(monteur as MonteurDeLabyrinthe) as Labyrinthe
 
    monteur.MonteLabyrinthe()
 
    monteur.MontePièce(1)
    monteur.MontePièce(2)
    monteur.MontePorte(1, 2)
 
    Return monteur.GetLabyrinthe()
 
End Function

Comparez cette version de NouveauLabyrinthe à l’originale. Notez bien comment le Monteur cache la représentation interne du labyrinthe (c’est-à-dire, les classes qui définissent les pièces, les portes et les murs), et comment ces parties sont assemblées pour produire le labyrinthe final. On peut deviner qu’il existe des classes pour les pièces et les portes, mais aucun indice ne laisse à penser qu’il en existe pour les murs. Cela permet de changer plus facilement la manière dont un labyrinthe est représentée, puisque aucun des clients de MonteurDeLabyrinthe n’a besoin d’être modifié.

Comme les autres motifs de création, le motif de conception Monteur encapsule la manière dont les objets sont créés, dans ce cas : au travers de l’interface définie par MonteurDeLabyrinthe. Cela signifie que l’on peut réutiliser MonteurDeLabyrinthe afin de construire différents types de labyrinthes. La fonction NouveauLabyrintheComplexe en donne un exemple :

Public Function NouveauLabyrintheComplexe(monteur as MonteurDeLabyrinthe) as Labyrinthe
 
    monteur.MonteLabyrinthe()
 
    monteur.MontePièce(1)
    monteur.MontePièce(2)
    '...
    monteur.MontePièce(1001)
 
    Return monteur.GetLabyrinthe()
 
End Function

Notez bien que MonteurDeLabyrinthe ne crée pas le labyrinthe lui-même ; son but principal est simplement de définir une interface pour la création de labyrinthes. Il définit une implémentation vide principalement par commodité. Les classes dérivées de MonteurDeLabyrinthe font le véritable travail.

La sous-classe MonteurDeLabyrintheStandard est une implémentation qui construit de simples labyrinthes. Elle garde une trace du labyrinthe qu’elle construit dans la variable _labyrintheCourant.

Class MonteurDeLabyrintheStandard
    Inherits MonteurDeLabyrinthe
 
    Private _labyrintheCourant as Labyrinthe
 
    Public Sub New()
        '...
    End Sub
 
    Public Shared Sub MonteLabyrinthe()
        '...
    End Sub
 
    Public Shared Sub MontePièce(ByVal numPièce as Integer)
        '...
    End Sub
 
    Public Shared Sub MontePorte(ByVal dePièce as Integer, ByVal àPièce as Integer)
        '...
    End Sub
 
    Public Shared Function GetLabyrinthe() as Labyrinthe
        '...
    End Function
 
    Private Function MurCommun(ByVal p1 as Pièce, ByVal p2 as Pièce) as Direction
        '...
    End Function
 
End Class

MurCommun est une fonction utilitaire déterminant la direction d’un mur commun entre deux pièces.

Le constructeur de MonteurDeLabyrintheStandard initialise simplement _labyrintheCourant.

Public Sub New()
    Me._labyrintheCourant = Nothing
End Sub

MonteLabyrinthe instancie un Labyrinthe que les autres opérations vont assembler et éventuellement retourne au client (avec GetLabyrinthe).

Public Sub MonteLabyrinthe()
    Me._labyrintheCourant = New Labyrinthe()
End Sub
 
Public Function GetLabyrinthe() as Labyrinthe
    Return Me._labyrintheCourant
End Function

La procédure MontePièce crée une pièce et construit les murs autour d’elle :

Public Shared Sub MontePièce(ByVal numPièce as Integer)
 
    If (Me._labyrintheCourant.PieceNo(numPièce) Is Nothing) Then
        Dim maPièce as Pièce = New Pièce(numPièce)
        Me._labyrintheCourant.AjouterPiece(maPièce)
        maPièce.Cote(Nord) = New Mur()
        maPièce.Cote(Sud) = New Mur()
        maPièce.Cote(Est) = New Mur()
        maPièce.Cote(Ouest) = New Mur()
    End If
 
End Sub

Pour construire une porte entre deux pièces, MonteurDeLabyrintheStandard prend les deux pièces dans le labyrinthe et trouve leur mur commun :

Public Shared Sub MontePorte(ByVal dePièce as Integer, ByVal àPièce as Integer)
 
    Dim p1 as Pièce = Me._labyrintheCourant.PieceNo(dePièce)
    Dim p2 as Pièce = Me._labyrintheCourant.PieceNo(àPièce)
    Dim po as Porte = New Porte(p1, p2)
 
    p1.Cote(MurCommun(p1, p2)) = po
    p2.Cote(MurCommun(p2, p1)) = po
 
End Function

Les clients peuvent maintenant utiliser NouveauLabyrinthe en conjonction avec MonteurDeLabyrintheStandard pour créer un labyrinthe :

Dim laby as Labyrinthe
Dim jeu as JeuDuLabyrinthe
Dim monteur as MonteurDeLabyrintheStandard
 
jeu.NouveauLabyrinthe(monteur)
laby = monteur.GetLabyrinthe()

On pourrait avoir mis toutes les opérations de MonteurDeLabyrintheStandard dans Labyrinthe et laissé chaque Labyrinthe se monter lui-même. Mais faire une plus petite classe Labyrinthe la rend plus simple à comprendre et à modifier, et MonteurDeLabyrintheStandard est facile à séparer de Labyrinthe. Plus important, séparer les deux permet d’avoir une multitude de MonteurDeLabyrinthe, chacun utilisant des classes différentes pour les pièces, les murs et les portes.

Un MonteurDeLabyrinthe plus exotique serait MonteurDeLabyrintheComptable. Ce monteur ne crée pas du tout de Labyrinthe ; il ne fait que compter les différents types de composants qui auraient été créés.

Class MonteurDeLabyrintheComptable
    Inherits MonteurDeLabyrinthe
 
    Private _portes as Integer
    Private _pieces as Integer
 
    Public Sub New()
        '...
    End Sub
 
    Public Shared Sub MonteLabyrinthe()
        '...
    End Sub
 
    Public Shared Sub MontePièce(ByVal numPièce as Integer)
        '...
    End Sub
 
    Public Shared Sub MontePorte(ByVal dePièce as Integer, ByVal àPièce as Integer)
        '...
    End Sub
 
    Public Shared Sub AjouterMur(ByVal numPièce as Integer, ByVal dir as Direction)
        '...
    End Sub
 
    Public Sub GetCompte(ByRef nbPortes as Integer, ByRef nbPièces as Integer)
        '...
    End Sub
 
End Class

Le constructeur initialise les compteurs, et les opérations surchargées de MonteurDeLabyrinthe s’occupent de les incrémenter comme il faut.

Public Sub New()
    Me._portes = 0
    Me._pieces = 0
End Sub
 
Public Shared Sub MontePièce(ByVal numPièce as Integer)
    Me._pieces = Me._pieces + 1
End Sub
 
Public Shared Sub MontePorte(ByVal dePièce as Integer, ByVal àPièce as Integer)
    Me._portes = Me._portes + 1
End Sub
 
Public Sub GetCompte(ByRef nbPortes as Integer, ByRef nbPièces as Integer)
    nbPortes = Me._portes
    nbPièces = Me._pieces
End Sub

Et voici comment un client pourrait utiliser un MonteurDeLabyrintheComptable :

Dim pieces as Integer
Dim portes as Integer
Dim jeu as JeuDuLabyrinthe
Dim monteur as MonteurDeLabyrintheComptable
 
jeu.NouveauLabyrinthe(monteur)
monteur.GetCompte(portes, pieces)
 
MsgBox("Le labyrinthe contient " & pieces & " pièces et " & portes & " portes.")

Utilisations connues

L’application de conversion RTF vient de ET++. Son bloc de construction de texte utilise un Monteur pour traiter le texte stocké au format RTF.

Monteur est un motif de conception commun en GNU Smalltalk :

  • La classe Parser du système interne de compilation est un Directeur qui prend un objet ProgramNodeBuilder en argument. Un objet Parser notifie son objet ProgramNodeBuilder à chaque fois qu’il reconnaît une construction syntaxique. Quand l’analyse est terminée, Parser demande au monteur l’arbre d’analyse qu’il a construit et le retourne au client.
  • ClassBuilder est un Monteur que les classes utilisent pour créer des sous-classes d’elles-même. Dans ce cas, une classe est à la fois le Directeur et le Produit.
  • ByteCodeStream est un Monteur qui crée une méthode compilée en tant que tableau d’octets. ByteCodeStream est une utilisation non-standard du mitif de conception Monteur, parce que l’objet complexe qu’il construit est encodé comme un tableau d’octets, et non comme un objet SmallTalk standard. Mais l’interface de ByteCodeStream est typique du Monteur, et il serait très simple de remplacer ByteCodeStream par une classe différente représentant un programme comme un objet composé.

Le framework « Service Configurator » d’Adaptive Communications Environment utilise un Monteur afin de construire les composants de service réseau liés à un serveur en cours d’exécution. Les composants sont décrits avec un langage de configuration qui est analysé par un parser LALR. Les actions sémantiques de l’analyseur effectuent des opérations sur le Monteur qui ajoutent des informations au composant de service. Dans ce cas, l’analyseur est le Directeur.

Voir Aussi

La Fabrique Abstraite est similaire au Monteur en ce sens qu’elle construit elle aussi des objets complexes. La différence principale est que le motif de création Monteur se focalise sur la construction d’objets complexes, étape par étape. Fabrique Abstraite met l’accent sur les familles d’objets produits (qu’ils soient simples ou complexes). Pour le motif de conception Monteur, le retour du produit est la phase finale, tandis que Fabrique Abstraite le retourne immédiatement.

Un Objet Composite est ce que construit un Monteur la plupart du temps.

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