Implémentation du modèle Singleton
Le Singleton est le modèle le plus souvent utilisé pour présenter le principe des modèles de conception. C’est l’un des plus faciles à mettre en œuvre, c’est pourquoi il est compréhensible et utilisable même pour des débutants. Il permet de garantir l’unicité de l’instance d’une classe et son accessibilité à partir de n’importe endroit du programme. Les cas de figure où une instance unique présente un intérêt sont multiples. Par exemple, dans la programmation d’un jeu, le moteur de son, chargé de gérer la lecture des sons et des musiques, doit être unique. Vu son utilisation omniprésente, il doit être accessible de partout. Ce modèle fait appel à plusieurs principes de la programmation orientée objet. L’idée principale du Singleton est que l’instance est englobée dans la classe. Elle sera donc un attribut de cette classe. Cet attribut devra être privé, ce qui empêchera son accès à tout élément extérieur à la classe. De plus, il devra être initialisé à Nothing pour que l’on puisse vérifier son existence. Pour pouvoir y accéder, on devra implémenter un accesseur. Cet accesseur devra être partagé. De cette manière, on pourra y accéder de partout, en invoquant la classe. De plus, le constructeur devra être privé. De cette manière, on empêchera une instanciation manuelle de l’objet. Or, si le constructeur est privé, comment créer l’instance de la classe ? Cela sera fait dans l’accesseur. Celui-ci devra vérifier si l’instance de la classe existe. Si elle existe, il la retournera. Dans le cas contraire, il l’instanciera et la retournera. Cet accesseur est la clé du modèle. En effet, c’est lui qui vérifie l’existence et l’unicité de l’instance, et la renvoie le cas échéant. Voici un schéma de ce modèle. La première étape consiste à déclarer la classe avec son constructeur privé, pour empêcher l’instanciation : Public Class Singleton ’ Le constructeur est privé, ’ on ne peut pas instancier la classe Private Sub New() End Sub End Class Le constructeur peut contenir d’autres opérations à effectuer, par rapport à l’utilisation de la classe. On crée ensuite l’instance de la classe. La particularité du Singleton est que l’instance est un attribut privé de cette classe. Celui-ci doit être Les modèles de conception Chapitre 9 LE GUIDE COMPLET 223 également partagé, car l’accesseur sera également partagé pour être accessible à partir de la classe, et non d’un objet. Il doit être initialisé à Nothing. De cette manière, on saura s’il existe ou pas. S’il vaut Nothing, cela signifie qu’il n’existe pas. Sinon il existe : Public Class Singleton ’ Le constructeur est privé, ’ on ne peut pas instancier la classe Private Sub New() End Sub ’ Instance unique de la classe ’ c’est un attribut privé de celle-ci Private Shared instance As Singleton = Nothing End Class Il faut ensuite implémenter l’accesseur qui permettra de récupérer cette instance. Si l’instance n’existe pas, c’est-à-dire si l’attribut instance vaut Nothing, cet accesseur doit créer l’instance. Il le pourra car il a accès au constructeur, même si celui-ci est privé. Ensuite, il retournera l’instance. Public Class Singleton ’ Le constructeur est privé, ’ on ne peut pas instancier la classe Private Sub New() End Sub ’ Instance unique de la classe ’ c’est un attribut privé de celle-ci Private Shared instance As Singleton = Nothing ’ L’accesseur crée l’instance si elle n’existe pas ’ Puis il la retourne Public Shared Function getInstance() As Singleton If (Me.instance Is Nothing) Me.instance = New Singleton() End If Return Me.instance End Function End Class Le Singleton est maintenant totalement défini. Pour pouvoir y avoir accès et l’utiliser, il suffit d’invoquer la classe et de faire appel à sa méthode getInstance : Singleton.getInstance()
Quelques modèles de conception courants
Le nombre des modèles de conception est considérable. Chacun d’entre eux résout une problématique donnée. Dans cette partie, nous survolerons quelques-uns des modèles de conception les plus courants. Nous n’entrerons pas dans les détails et ne les implémenterons pas non plus, car ce n’est pas le sujet de cet ouvrage. Cependant, c’est un aspect important de la programmation, car ce sont des techniques valables, éprouvées, efficaces et fiables. DisposeFinalize Vous devriez pour l’instant laisser le ramasse-miettes s’occuper de la libération de la mémoire, car vous risqueriez d’être moins efficace que lui en tentant de forcer vous-même la libération. Effectivement, libérer la mémoire proprement n’est pas chose aisée. L’implémentation du modèle DisposeFinalize permet de bien le faire. Il est composé de plusieurs éléments : j Il faut tout d’abord un attribut booléen pour déterminer si la destruction a été faite. j Puis, on doit avoir une fonction Dispose. Celle-ci fait appel à une fonction surchargée prenant en paramètre un booléen. Ce booléen détermine si c’est vous qui avez forcé la destruction de l’objet, ou si c’est le ramasse-miettes. La fonction Dispose sans paramètre appelle la fonction Dispose avec le paramètre true, puis la fonction SuppressFinalize du ramasse-miettes. j Ensuite, il faut redéfinir la fonction Finalize. Elle appellera la fonction Dispose avec le paramètre false pour prévenir que c’est le ramasse-miettes qui a lancé la destruction. Si la classe en cours est une classe fille, il faut libérer la classe mère. j Dans la classe Dispose surchargée, selon le paramètre, le traitement de la libération des différents attributs devra être faite. Si vous implémentez ce modèle, vous pourrez alors détruire vous-même vos objets en faisant appel à la méthode Dispose. Elle se chargera de libérer la mémoire dédié à l’objet et aux autres objets qui dépendent de lui. 226 LE GUIDE COMPLET Chapitre 9 Passer au niveau supérieur Iterator Lorsque les projets sont relativement conséquents, les structures de données complexes, il peut être fastidieux de les parcourir dans leur globalité. En effet, on ne peut pas toujours se contenter d’utiliser une simple boucle for, en partant du premier au dernier élément, comme dans les tableaux. Le modèle Iterator permet justement de parcourir des classes structurées complexes, comme si elles n’étaient que de simples listes. De plus, utiliser ce modèle permet de parcourir la structure, mais sans exposer son fonctionnement intérieur. Pour cela, on a besoin de trois éléments : j Il faut une interface définissant un itérateur. Cette interface contient au moins une méthode permettant d’accéder à l’élément courant, ainsi qu’une méthode permettant d’accéder à l’élément suivant. On peut compléter cette interface en ajoutant la possibilité de supprimer l’élément en cours, mais l’indispensable est de pouvoir accéder à l’élément en cours et au suivant. j Il faut une classe MonIterator adaptée à la structure qui implémente l’interface Iterator. Cette classe saura parcourir la structure. j Dans la classe correspondant à une structure, il faut une méthode qui crée et renvoie un itérateur capable de parcourir cette structure. Vous pourrez alors parcourir cette structure sans vous soucier de son fonctionnement, étant donné que l’itérateur est fait pour cela, et adapté à ladite structure. État Les comportements d’un objet dépendent souvent d’un certain état, à un moment donné. Un magasin par exemple est susceptible d’accueillir des clients s’il est dans un état « ouvert ». S’il est dans un état « fermé », il n’y a pas de clients dedans et les lumières sont éteintes. La notion d’état détermine donc un certain comportement de l’objet à un moment donné. Le rôle de ce modèle est de symboliser cette notion d’état, sans toucher à l’objet lui-même. De cette manière, on ne rend pas les états dépendant de l’objet. Si par exemple on doit ajouter ou supprimer des états, il ne sera pas nécessaire de remodifier totalement l’objet lui-même. Les modèles de conception Chapitre 9 LE GUIDE COMPLET 227 La méthode communément utilisée pour ce problème est de définir les états comme des constantes (ETAT_OUVERT, ETAT_FERME), d’avoir un attribut symbolisant l’état en cours, et selon la valeur de cet attribut, de faire les opérations ad hoc. Si le nombre d’états est important, le code peut devenir confus. Le principe de ce modèle est de définir une interface correspondant à un état. Ensuite, chaque état sera une classe implémentant cette interface. j Tout d’abord, il faut créer l’interface. Cette interface contiendra l’ensemble des méthodes en rapport avec l’objet, par exemple pour le magasin servirClient, rangerMateriel, etc., c’està-dire l’ensemble des méthodes qui ont une dépendance par rapport à un état donné. j Ensuite, il faut créer les classes des différents états et leur faire implémenter l’interface. Par exemple, pour l’état magasinFerme, la classe retournera une exception, alors que pour magasinOuvert, elle effectuera l’opération comme il faut. j Il faut enfin décrire le mécanisme de changement d’état, permettant de passer de l’un à l’autre et de définir un état de départ. De cette manière, vous fournissez à l’objet un comportement qui dépend d’un état donné. Or, ce traitement est séparé de l’objet lui-même, ce qui permet de gagner en compréhension, en souplesse et en fiabilité. L’étude des modèles de conception est bénéfique pour la compréhension des mécanismes de la programmation orientée objet. Nous en avons présenté ici trois qui sont relativement simples. D’autres sont plus complexes et il en existe une multitude, chacun adapté à un problème donné. N’hésitez pas à en utiliser, vos programmes seront de meilleure qualité. 9.5. Quelques bonnes habitudes à prendre Un bon programme repose sur les habitudes de son programmeur. Il est rare d’avoir un programme de qualité si celui qui l’a conçu l’a fait de manière désinvolte, sans faire attention. Nous exposerons ici quelques bonnes habitudes à prendre, que vous avez mises en œuvre pour la plupart. Vous comprendrez tout leur intérêt et vous les aurez sous la main. Passer au niveau supérieur Pour une meilleure compréhension, bien indenter L’indentation consiste à commencer des lignes du même bloc au même niveau. On utilise des tabulations pour les décaler. Cela permet une meilleure compréhension dans le code source. La règle est : on entre dans un bloc, on indente, on sort d’un bloc, on revient au niveau de départ. Un bloc peut être la déclaration d’une fonction, une structure de contrôle, etc. Public Sub testIndent(ByVal ind As Boolean) If ind MessageBox.Show(« Le paramètre est vrai ») Else MessageBox.Show(« Le paramètre est faux ») End If End Sub Vous avez ici trois niveaux d’indentation : j La déclaration de la fonction, alignée à gauche. j Le contenu de la fonction, qui est décalé d’une tabulation après la déclaration de celle-ci. À ce niveau seront alignées toutes les instructions de la fonction, dont le If qui ouvre un nouveau bloc. j L’intérieur des blocs du If et du Else, contenant les instructions d’affichage. L’indentation permet un gain réel de lisibilité. Voici un code sans indentation : Public Sub testIndent(ByVal ind As Boolean) If ind MessageBox.Show(« Le paramètre est vrai ») Else MessageBox.Show(« Le paramètre est faux ») End If End Sub Ce bout de programme reste compréhensible car il est petit. Mais il est beaucoup moins lisible ainsi. C’est pourquoi nous vous recommandons de bien indenter vos programmes. Quelques bonnes habitudes à prendre 9 Être clair et expliquer Soyez explicite. De cette manière, en lisant le code source ultérieurement, vous comprendrez son sens et son intérêt. Prenons le Singleton examiné précédemment : Public Class Singleton ’ Le constructeur est privé, ’ on ne peut pas instancier la classe Private Sub New() End Sub ’ Instance unique de la classe ’ c’est un attribut privé de celle-ci Private Shared instance As Singleton = Nothing ’ L’accesseur crée l’instance si elle n’existe pas ’ Puis il la retourne Public Shared Function getInstance() As Singleton If (Me.instance Is Nothin) Me.instance = New Singleton() End If Return Me.instance End Function Public nom As String = « Toto » End Class Cette classe est claire, explicite et commentée. Grâce aux noms des différents éléments, on comprend à quoi ils correspondent. Vous pouvez vous reporter aux commentaires pour chercher des compléments d’information. Voici la même classe, sans commentaire, avec des noms qui ne sont pas explicites du tout : Public Class Ljopusd Private Sub New() End Sub Private Shared utjvqsd As Ljopusd = Nothing Public Shared Function oieurIqsg() As Ljopusd If (Me.utjvqsd Is Nothing) Me. utjvqsd = New Ljopusd() End If Return Me.utjvqsd End Function Public pisudgsglqjef As String = « Toto » End Class 230 LE GUIDE COMPLET Chapitre 9 Passer au niveau supérieur On a du mal à s’imaginer que cela est un Singleton. Pourtant, c’est exactement la même classe que la précédente, avec un comportement identique. D’où l’intérêt d’être explicite et de mettre des commentaires. Tester les éléments séparément d’abord Quand vous testez l’application, faites-le élément par élément. On ne sait jamais, il est possible qu’un défaut quelque part soit compensé par un autre défaut ailleurs. Prenons un cas évident : Public Function addition1(ByVal nbr1 As Integer, _ ByVal nbr2 As Integer) Return nbr1 + nbr2 + 1 End Function Public Function addition2 (ByVal nbr1 As Integer, _ ByVal nbr2 As Integer) Return nbr1 + nbr2 – 1 End Function Ces deux fonctions effectuent une addition. Elles sont toutes les deux fausses au niveau du sens. Testez-les ainsi : Dim res As Integer res = addition1(3, 4) + addition2(6, 9) MessageBox.Show(« res= »& res) Vous obtiendrez effectivement le bon résultat car les deux erreurs s’annulent. Pourtant les deux fonctions sont fausses. C’est pourquoi, il faut faire les tests des éléments séparément : MessageBox.Show(« somme= »& addition1(3, 4)) MessageBox.Show(« somme= »& addition2(6, 9)) Figure 9.43 : Résultat correct avec des fonctions fausses Figure 9.44 : Erreur sur addition1 Quelques bonnes habitudes à prendre Chapitre 9 LE GUIDE COMPLET 231 De cette manière, vous vous rendrez compte que les deux fonctions sont fausses, et en plus vous pourrez comprendre l’erreur.