Design Patterns : Motifs de Création

novembre26, 2007

Cet article est le premier d’une longue série : j’attaque la traduction du livre sur les Design Patterns écrit par le Gang Of Four…

Les motifs de Création permettent l’abstraction du processus d’instanciation. Ils aident à rendre le système indépendant de la manière dont les objets sont créés, assemblés et représentés. Une classe de motif de création utilise l’héritage afin de varier les classes devant être instanciées, tandis qu’un objet de motif de création va déléguer l’instanciation à un autre objet.

Les motifs de création gagnent en importance quand le système évolue afin de dépendre d’avantage de l’assemblage des objets plutôt que de l’héritage de classes. Durant cette transformation, on passe d’un code « en dur » avec un ensemble de comportements figés, vers un ensemble restreint de comportements fondamentaux qui peuvent être assemblés en de nombreux comportements plus complexes. Créer de cette manière des objets avec un comportement particulier demande plus qu’une simple instanciation de classe.

On retrouve deux thèmes réccurents dans ces motifs. Tout d’abord, ils encapsule la la visibilité de la classe concrète utilisée par le système. Ensuite, ils cachent la manière dont les instances de ces classes sont créées et mises ensemble. Tout ce que les systèmes connaissent des objets qu’ils utilisent, c’est leur interface telle qu’elle est définie par des classes abstraites. En conséquence, les motifs de création offrent une grande flexibilitésur ce qui est créé, qui le crée, comment c’est créé et quand. Ils permettent de configurer un système avec des objets « de production » variant énormément en termes de structures et de fonctionnalités. La configuration peut être statique (définie à la compilation) ou dynamique (au chargement).

Parfois, les motifs de création entrent en compétition. Par exemple, on trouve des cas où les motifs Prototype ou Fabrique Abstraite (Abstract Factory) seraient tous les deux utiles. À d’autres moments, ils sont complémentaires : Monteur (Builder) peut utiliser d’autres motifs pour implémenter quel composant doit être créé. Prototype peut utiliser Singleton dans son implémentation.

Comme les motifs de création sont très proches les uns des autres, nous allons étudier les cinq ensemble afin de souligner leurs points communs et leurs différences. Nous allons également utiliser un exemple commun (construire un labyrinthe pour les besoins d’un jeu) afin d’illustrer leurs implémentation. Le labyrinthe et le jeu vont légèrement varier d’un motif à l’autre. Parfois, le jeu consistera simplement à trouver son chemin dans le labyrinthe ; dans ce cas le joueur n’aura probablement qu’une vue restreinte du labyrinthe. D’autres fois, le labyrinthe contiendra des énigmes à résoudre et des dangers à éviter, et ces jeux fourniront une carte de la partie du labyrinthe ayant été explorée.

On ignorera de nombreux détails sur ce qui peut être dans le labyrinthe, ou si le jeu a un ou plusieurs joueurs. Au lieu de cela, nous nous focaliserons sur la manière dont le labyrinthe est créé. On définit un labyrinthe comme un ensemble de pièces. Une pièce connaît son voisinage ; ses voisins peuvent être une autre pièce, un mur, ou une porte vers une autre pièce.

Les classes Pièce, Porte et Mur définissent les composants du labyrinthe utilisés dans tous nos exemples. Nous ne définirons que les parties de ces classes importantes pour créer un labyrinthe. Nous ignorerons les joueurs, les méthodes d’affichage et d’exploration du labyrinthe, et d’autres fonctionnalités importantes qui ne sont pas essentielles pour construire le labyrinthe.

Le schéma suivant montre les relations entre ces classes :

Schéma d’exemple

Chaque pièce a quatre côtés. Nous utiliserons une énumération appelée Direction pour spécifier les faces nord, sud, est et ouest d’une pièce :

Enum Direction
 
  Nord
 
  Sud
 
  Est
 
  Ouest
 
End Enum

La classe ComposantLabyrinthe est la classe abstraite commune à tous les composants d’un labyrinthe. Pour simplifier l’exemple, ComposantLabyrinthe ne définit qu’une méthode, Entrer. Sons sens dépend de ce dans quoi vous entrez. Si vous entrez dans une pièce, votre localisation change. Si vous essayez d’entrer dans une portes, deux solutions : soit la porte est ouverte et vous changez de pièce, soit la porte est fermée et vous vous cognez le nez.

MustInherit Class ComposantLabyrinthe
 
  Public MustOverride Sub Entrer()
 
End Class

Entrer fournit une simple base pour des actions de jeu plus sophistiquées. Par exemple, si vous êtes dans une pièce et que vous dites « Aller à l’Est », le jeu peut déterminer simplement quel ComposantLabyrinthe est positionné à l’est, et appeler sa méthode Entrer. La méthode Entrer spécifique à la sous-classe va s’occuper de savoir si vous allez changer de pièce ou si vous allez avoir mal au nez. Dans un vrai jeu, Entrer pourrait prendre en argument l’objet Joueur en train de bouger. Pièce est la sous-classe concrète de ComposantLabyrinthe qui définit les relations-clés entre les différents composants du labyrinthe. Elle maintient les références aux autres composants et enregistre un numéro de pièce. Le numéro va identifier les pièces du labyrinthe.

Class Pièce
 
    Inherits ComposantLabyrinthe    Private _cotes(4) As ComposantLabyrinthe
 
    Private _numero As Integer
 
Public Property Cote(ByVal dir As Direction) As ComposantLabyrinthe
 
        Get
 
            Return _cotes(dir)
 
        End Get
 
        Set(ByVal value As ComposantLabyrinthe)
 
            _cotes(dir) = value
 
        End Set
 
    End Property
 
Public ReadOnly Property Numero() As Integer
 
        Get
 
            Return _numero
 
        End Get
 
    End Property
 
Public Sub New(ByVal numero As Integer)
 
        _numero = numero
 
    End Sub
 
Public Overrides Sub Entrer()
 
        '...
 
    End Sub
 
End Class

Les classes suivantes représentent les murs et les portes.

Class Mur
 
    Inherits ComposantLabyrinthe    Public Sub New()
 
        '...
 
    End Sub
 
Public Overrides Sub Entrer()
 
        '...
 
    End Sub
 
End Class
 
Class Porte
 
    Inherits ComposantLabyrinthe
 
Private _piece1 As Pièce
 
    Private _piece2 As Pièce
 
    Private _ouverte As Boolean
 
Public Sub New(ByVal p1 As Pièce, ByVal p2 As Pièce)
 
        _piece1 = p1
 
        _piece2 = p2
 
        _ouverte = False
 
    End Sub
 
Public Function AutreCote(ByVal p As Pièce) As Pièce
 
        If p Is _piece1 Then
 
            Return _piece2
 
        ElseIf p Is _piece2 Then
 
            Return _piece1
 
        End If
 
        Return Nothing
 
    End Function
 
Public Overrides Sub Entrer()
 
        '...
 
    End Sub
 
End Class

On a besoin de connaître plus qu’une simple partie du labyrinthe. On définit en plus une classe Labyrinthe qui représente une collection de pièces. Labyrinthe peut aussi trouver une pièce donnée à partir de son numéro, en utilisant la méthode PieceNo.

Class Labyrinthe
 
    Private _pieces() As Pièce    Public Sub New()
 
        '...
 
    End Sub
 
Public Sub AjouterPiece(ByVal p As Pièce)
 
        '...
 
    End Sub
 
Public Function PieceNo(ByVal n As Integer) As Pièce
 
        For Each p As Pièce In _pieces
 
            If p.Numero = n Then
 
                Return p
 
            End If
 
        Next p
 
        Return Nothing
 
    End Function
 
End Class

PieceNo peut faire une recherche par dictionnaire, table de hashage, ou un simple tableau comme ci-dessus. On ne se préoccupe pas de ces détails ici. Au lieu de ça, nous allons nous focaliser sur la manière de spécifier les composants d’un objet Labyrinthe.

Une autre classe à définir va être JeuDuLabyrinthe, qui va créer le labyrinthe. Une manière directe de créer le labyrinthe va être de spécifier une série d’opérations pour ajouter des composants à un labyrinthe et les connecter entre eux. Par exemple, la function suivante va créer un labyrinthe composé de deux pièces avec une porte entre les deux :

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

Cette fonction est quelque peu compliquée si l’on considère qu’elle ne fait que créer un labyrinthe à deux pièces. On pourrait évidemment simplifier cela. Par exemple, le constructeur de Pièce pourrait initialiser les côtés par un mur dès le départ. Mais ça ne ferait que déplacer le code ailleurs. Le vrai problème avec cette fonction ne vient pas de sa taille mais plutôt de son manque de flexibilité. On code en dur le schéma du labyrinthe. Changer de schéma signifie changer de fonction, soit en la surchargeant (ce qui implique de tout réimplémenter) soit en la modifiant en partie (ce qui est générateur d’erreur et empêche la réutilisation).

Les motifs de création montrent comment rendre ceci plus flexible, mais pas nécessairement plus court. En particulier, ils vont rendre simple le changement de classes définissant les composants d’un labyrinthe.

Supposons que vous vouliez réutiliser un schéma de labyrinthe existant pour un nouveau jeu contenant (entre autre) des labyrinthes ensorcellés. Le jeu de labyrinthe ensorcellé va avoir de nouveaux type de composants, comme PorteAvecSort, une porte verrouillée qui ne peut être ouverte qu’en utilisant un sort ; et PièceEnsorcelée, une pièce qui peut contenir des articles non conventionnels, comme des sorts ou des clés magiques. Comment pourriez-vous modifier simplement CreerLabyrinthe afin que cette fonction crée des labyrinthes avec ces nouveaux objets ?

Dans ce cas, le plus gros obstacle au changement résulte de l’appel « en dur » des classes à instancier. Les motifs de création fournissent des différentes manières de supprimer les références explicites aux classes concrètes du code devant les instancier :

  • Si NouveauLabyrinthe fait appelle à des fonctions virtuelles au lieu des constructeurs pour créer les pièces, les murs, et les portes requises, alors vous pourrez changer les classes instanciées en construisant une sous-classe de JeuDuLabyrinthe et en redéfinissant ces fonctions virtuelles. Cette approche est un exemple du motif Fabrique.
  • Si on passe en paramètre à NouveauLabyrinthe un objet utilisé pour créer les pièces, les murs et les portes, alors on peut changer les classes pièces, murs et portes à utiliser en passant un paramètre différent. C’est un exemple du motif Fabrique Abstraite (Abstract Factory)
  • Si NouveauLabyrinthe reçoit un objet qui peut créer entièrement un nouveau labyrinthe en utilisant des fonctions pour ajouter les pièces, les portes et les murs au labyrinthe qu’il construit, alors vous pourrez changer des parties du labyrinthe, où la manière dont il est monté. C’est un exemple du motif Monteur (Builder).
  • Si NouveauLabyrinthe est paramétrés par de nombreux objets prototypés pièce, portes et murs, qu’il copie et ajoute au labyrinthe, alors vous pourrez changer la composition du labyrinthe en remplaçant ces objets prototypés par d’autres. C’est un exemple du motif Prototype.

Le motif de création restant, Singleton, permet de s’assurer qu’il n’existe qu’un seul labyrinthe par jeu, et que chaque objet jeu a accès à lui (sans recourir à une variable globale ou une fonction globale). Singleton permet aussi facilement d’étendre ou de remplacer le labyrinthe sans toucher au code existant.

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