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 :
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.