POURQUOI CE BLOG, POUR QUI ?


POURQUOI CE BLOG, POUR QUI ?
Ce Blog s'adresse à tous ceux qui sont passionnés par les sciences informatiques , Professionnels,Etudiants,Amateurs ...
Les sujets exposés dans la suite se rapporteront essentiellement sur l'analyse informatique,la programmation,le développement ainsi que à l'architecture IT.
QUI SUIS JE ?
Je suis Kangulungu Lubilanji, Consultant-Freelance sur les technologies .NET,C#,ASP.NET ... Contactez moi pour plus d'informations.

La Programmation OO: Chapitre 20. Design patterns

Chapitre 20. Design patterns
Les desing patterns sont des recettes de conception OO qui à l'origine furent réunies dans le célèbre livre du gang des quatre. Ils sont depuis devenus presque aussi courants que les langages de programmation et UML. Leur connaissance complète toute formation approfondie en OO et permet une meilleure compréhension des mécanismes de ce style de programmation. Ce chapitre en décrit quelques-uns à l'aide d'exemples concrets.

Introduction aux design patterns, dans toute discipline, quelle qu'elle soit, l'expert est celui qui à développé un flair suffisant pour identifier rapidement les problèmes types et qui sait comment les resoudres facilement et vite. En 1995, le GOF (Gang Of Four), a transposé la pratique des design patterns architecturaux dans l'univers du logiciel.
Si le recours à ces patterns ne semble devoir intervenir q'une fois le problème rencontré et pas avant, il n'en reste pas moins vrai que leur maîtrise permet de réorienter, et cela dès le début, la conception du code en leur donnant une place prépondérante. Cette réorientation ne peut être que bénéfique puisqu'elle reprend la longue expérience des nombreux développeurs antérieurs. Certains vont même jusqu'à parler de développement par les « patterns » ou à partir d'eux, se limitant à une simple combinaison de ceux-ci.
Le livre du GOF contient vingt-trois design patterns, dans ce livre chaque pattern est présenté par son nom, associé au problème qui motive son recours,assorti de la solution proposée ainsi que des limites de sa mise en oeuvre.
Toujours dans ce même livre, les patterns sont organisés en trois catégories : 

« créationnels », c'est-à-dire plutôt centrés sur la problématique de la construction d'objets grâce à l'amplification ou au court-circuit du constructeur ; 

« structurels », c'est-à-dire plutôt centrés sur les schémas associatifs des classes, afin, par exemple, de contrebalancer l'héritage et l'association ; 

« comportementaux », c'est-à-dire décrivant plutôt des mécanismes astucieux à mettre en oeuvre pour l'exécution des codes  ;

Le pattern singleton 
Dans la vie réelle, de nombreuses classes n'ont parfois comme raison  d'être que la création d'un et un seul objet.Une première possibilité serait d'en faire des classes statiques, comme il  en existe d'ailleurs pas mal dans la programmation OO. Dans un  soucis de cohérence, le GOF propose la mise au point d'une classe, dite classe « singleton », qui ne peut donner naissance qu'à une seule instance et garantir l'accès unique qu'à cette instance. Supposons que notre programme de flipper ne doive fonctionner qu'avec une seule boule. Le  code qui suit vous permet de réaliser cela grâce à une classe SingletonBoule, qui ne permettra qu'à une seule boule de se balader dans notre flipper.


class SingletonBoule : Boule {
        private static SingletonBoule b = null;  // notez le static
        private static Object ob = new Object();

        private SingletonBoule(int x, int y, int r) : base(x,y,r) {  // le constructeur doit être privé
          
        }
        public  static SingletonBoule getBoule(int x, int y, int r) {
            if (b == null)
            {
                b = new SingletonBoule(x, y, r);
            }
            return b;
        }
    }
    class Boule {
        int x, y, r;
        public Boule(int x, int y, int r) {
            this.x = x;
            this.y = y;
            this.r = r;
        }
    }

Le pattern Adaptateur, supposons que dans le code original du flipper, nous utilisons l'interface ObstacleDuFlipper dont une des méthodes abstraites dessineToi(Graphics g), à redéfinir dans les classes précises d'obstacles, est responsable de l'apparition  et de l'apparence de cet obstacle. Supposons qu'un autre développeur nous propose de fournir pour l'obstacle Champignon une classe de sa fabrication, dénommée ChampignonJoli, dont l'énorme avantage est de proposer une méthode sophistiquée pour dessiner magnifiquement ce type d'obstacle et nommée public void apparaitBeauChampignon(Graphics g, Rectangle r). Grâce aux deux diagrammes UML ci-dessous l'un misant sur l'héritage l'autre sur la composition - vous comprendrez aisément comment, sans modifier d'aucune manière le code d'origine, celui-ci pourra exploiter cette méthode. La manière dont la méthode dessineToi doit être redéfinie est indiquée pour les deux versions dans une note accolée à la classe Champignon. La classe qui sert d'intermédiaire entre l'interface de départ - celle que le flipper est contraint et forcé d'utiliser - et la classe contenant la méthode que nous souhaitons récupérer est Adaptateur. Il s'agit ici de la classe Champignon. Cet exemple montre comment, dans de nombreux cas, l'héritage et la composition peuvent apparaître comme alternative pour un  même problème.
Par héritage :
Extrait de code des classes et interface :
  interface ObstacleDuFlipper {
         void dessineToi(Graphics g);
    }
    class Champignon :ChampignonJoli, ObstacleDuFlipper {

        public void dessineToi(Graphics g)
        {
            Rectangle r  = new Rectangle ();
            apparaitBeauChampignon(g, r);
        }
    }
    class ChampignonJoli 
    {
        public void apparaitBeauChampignon(Graphics g, Rectangle r)
        { 
            //code
        }
    }
Par composition :
Extrait de code des classes et interface :
 interface ObstacleDuFlipper {
         void dessineToi(Graphics g);
    }
    class Champignon  {
        ChampignonJoli cj;
        public Champignon() { 
            cj = new ChampignonJoli();
        }
        public void dessineToi(Graphics g)
        {
            Rectangle r = new Rectangle ();
            cj.apparaitBeauChampignon(g, r);
        }
    }
    class ChampignonJoli 
    {
        public void apparaitBeauChampignon(Graphics g, Rectangle r)
        { 
            //code
        }
    }

Les Patterns patron de méthode,proxy, observer et memento, l'idée de pattern « patron de méthode » est de fournir un squelette de code dans lequel, bien qu'utilisée, une méthode agit sans son code. Il suffit alors au programmeur désirant exploiter ce squelette d'implémenter le corps de la méthode. Par exemple par redéfinition d'une méthode provenant d'une interface,afin de bénéficier de son squelette. Celui-ci sera complété et adapté à ce corps. Il se caractérise donc par une partie fixe, proposé par un premier développeur et par un partie variable (que fixe un  deuxième développeur). Cette seconde partie se résume au corps de la méthode à définir. Elle est variable car elle dépend de la définition du corps.

Si, dans le flipper, les obstacles sont installés dans un vecteur, et qu'il nous viens l'envie de les réordonner dans ce même vecteur, la seule information qui fait défaut est le critère selon les obstacles doivent se comparer et se réordonner. Toute la pratique de comparaison peut être mise en oeuvre, à l'exception de ce critère de comparaison qui constitue ici, en fait, le corps manquant de la méthode.

Pour le pattern « proxy », la présence du stub côté client, appelé à jouer le rôle de serveur, en est l'utilisation la plus connue. Le proxy se substitue à un objet manquant ou distant, afin de ne pas modifier radicalement le code de son interlocuteur, tout en apportant quelques fonctionnalités additionnelles indispensables au fonctionnement de l'objet distant. Un proxy  serait le bienvenu si parmi les obstacles du flipper, l'un d'entre eux prend énormément de temps à se dessiner car il doit chercher sur le web une image particulière. Comme indiqué dans la figure ci-dessous, et de manière très semblable au  pattern « adaptateur », il est possible de remplacer cet obstacle par un proxy qui, tout en déclenchant la méthode appropriée sur l'obstacle de départ, ajoute quelques fonctionnalités qui permettent de patienter, par exemple un message signalant que la recherche ou le téléchargement sont en cours.

 Le but pattern « observer » est de maintenir deux objets synchronisés, bien qu'ils ne possèdent pas entre eux de lien explicite.

Enfin le pattern « memento » vous permet de revenir à la case départ si un ensemble de manipulation d'objet n'aboutissent pas et qu'il devient important de restaurer les objets modifiés dans leurs états d'origine. Il suffit soit de le stocker au départ des manipulations sur le disque dur par une des méthodes décrites dans le chapitre 19, soit de les cloner et de conserver ces copies pendant toutes la durée des manipulations. En général, la classe de l'objet à mémoriser intègre un mécanisme de génération d'un « memento », qui ne conserve que les attributs susceptibles de modifications et qu'il est important de pouvoir restituer.

Le pattern flyweight, prenons le cas d'une molécule qui comme chacun sait, est un réseau d'atomes. Chaque atome possède des informations qui lui sont propres, reprises dans la classe : son symbole, son poids atomique, sa valence, sa charge éventuelle, etc. Une première solution rapide pour réaliser la classe Molecule aurait pu se limiter à une relation de composition entre la classe Molecule et la classe Atome. Cependant, lorsqu'on sait qu'une même molécule organique peut contenir jusqu'à des millions d'instances du même atome de carbone, il est stupide, et surtout extrêmement coûteux en mémoire, de répliquer pour chacune de ces instances toutes les informations comme le poids, la valence ou le symbole. Le remède à cela, application directe du pattern « flyweight » dont le but essentiel est de réduire l'espace de stockage des objets, consiste, comme indiqué ci-dessous à regrouper toutes ces informations dans une classe à part. L'économie de mémoire peut très vite s'avérer considérable. Nous retrouvons ce même besoin d'économie lorsque les cellules du système immunitaire se clonent. Il n'est sans doute pas nécessaire de stocker dans chaque clone l'entièreté de l'information. Ainsi, le même patrimoine génétique se retrouve pour l'essentiel dans toutes les cellules, ce qui autorise à ne le stocker qu'une fois en mémoire, toutes les cellule pouvant alors y faire référence.
Les patterns builder et prototype, un type d'objet dont la construction s'avère délicate dans le réacteur chimique sont les molécules. De nouvelles molécules apparaissent et disparaissent au cours de la simulation, résultant des réactions impliquant celles s'y trouvant au départ. En général, le programme lit les molécules de départ à partir d'un fichier ou d'une fenêtre initiale, dans lesquels celles-ci sont décrites à l'aide de leur symbole : CH4 ou NH3. Il faut alors pouvoir transcrire ces formules en un graphe canonisé, car c'est sous cette forme qu'elles seront ensuite, car elle exige une lecture et un traitement attentif des expressions définissant les molécules initiales.

Lorsqu'une réaction se produit, de nouvelles molécules apparaissent. Là encore, la génération d'une nouvelle molécule exige de nombreuses manipulations des molécules de départ.

La solution qui vient à l'esprit consiste à prévoir et à installer tous ces traitements dans le ou les constructeurs de la classe Molecule. La solution préconisée par le GOF est tout autre. Dès que la genèse de nouveaux objets exige une séquence de traitements compliqués (par exemple ; lecture et parsing d'un chaîne de caractères), il propose de séparer cette séquence de la construction de la molécule à proprement parler et de l'installer dans une classe à part, ici la classe MolecularBuilder (voir code ci-dessous), dont une  ou plusieurs méthodes renverrons une nouvelle instance de la classe Molecule, aboutissement de ces nombreux traitements. Le constructeur de la classe Molecule se limite à une création très élémentaire de l'objet, précédant ou concluant tous ces traitements.
 public class MoleculeBuilder { 
        // code
        public static Molecule build(string s) {
            Molecule m = new Molecule();
            // de nombreuses instructions de manipulation de « m » afin de construire la nouvelle molécule
            return m;
        }
    }
    public class Molecule { 
        //code
    }
... Et quelque part dans le main, on crée une nouvellemolécule : 
     m = MoleculeBuilder .build(s)
Lorsqu'une nouvelle molécule apparaît à l'issue d'une réaction comme celle-ci : 
AB + CD --> AC + BD,
cette nouvelle venue ne se construit pas à partir de rien, mais récupère les atomes des molécules de départ, avant la réaction. Ainsi, dans la réaction qui précède, la molécule AC est en partie une combinaison des molécules AB et CD. La manière la plus simple de générer le nouvelle venue à partir de ces deux clones. Cette démarche nous conduit tout droit au pattern « prototype » donc c'est la raison d'être : construire un nouvel objet à partir du clone d'un objet existant. Lorsqu'on clone une molécule, c'est en fait son graphe que l'on clone, en parcourant tous les noeuds atomiques de celui-ci. Le clonage d'un noeud atomique est donc défini récursivement et entraîne le clonage de tous les noeuds atomiques avec lesquels ce premier noeuds est connecté.

Le pattern façade, comme parfaitement illustré par les diagrammes de classe et de séquence de la figure qui suit, ce pattern dissimule un ensemble d'objets sous une interface unique, la seule avec laquelle l'utilisateur de tous ces objets interagira. C'est cette interface unifiée qui joue le rôle de « facade ». Elle permet une interface simplifiée avec cet ensemble d'objets ainsi qu'un point de contact unique. L'interlocuteur n'a nullement besoin de connaître la façon dons tous les objets agissent pour satisfaire les services proposés par l'interface. C'est la version logicielle de l'arbre qui cache la forêt. 

Le pattern command, supposons que l'on souhaite enrichir l'interface du flipper avec quelques menus et quelques boutons. Chaque « item » du menu et chaque bouton se voit associer une fonctionnalité donnée, comme c'est le cas dans le code Java classique qui suit :
public void actionPerformed(ActionEvent evt){
  Object obj = evt.getSource( );
   if (obj == mOpen){ // item de menu "mOpen"
      ....
   }
 if (obj == mSave){ // item de menu "mSave"
      ....
   }
if (obj == mStarting){ // item de menu "mSave"
      ....
   }
  ....
}
Les boutons et menus auront été préalablement créés et associés à une interface ActionListener comme suit pour le seul bouton blanc :

unBoutonBlanc = new Button("blanc") ;
unBoutonBlanc.addActionListener(this) ;

Le corps de la méthode actionPerformed reprend toutes les fonctionnalités associées à ces éléments de l'interface. Cette façon de procéder est loin d'être la meilleurs, et ce pour deux raisons principales. D'abord la présence de cette séquence de test (on aurait pu tout aussi bien utiliser un  « switch » à la place) dont le principal défaut est le manque de stabilité si l'idée nous viens d'ajouter quoi que ce soit dans l'interface. Dès qu'une suite de tests conditionnels ou un switch apparaît, il est toujours bon de vous demander, s'il n'y aurait pas lieu de remplacer le tout par un structure d'héritage et un mécanisme de polymorphisme. Si c'est le cas, vous accroîtrez en tout état de cause la stabilité du code face à des ajouts ou à des retraits des conditions antérieures dans le test.

La deuxième raison est que la méthode actionPerformed() est encombrée avec toutes les fonctionnalités associées à chacun des éléments de l'interface, ce qui la rend lourde et multiforme, c'est à dire peu en phase avec la simplicité et la modularité derrière lesquelles courent les développeurs OO.

L'alternative est l'application du pattern « command », dans lequel chaque tâche de l'interface se définit dans une classe à part qui implémente une interface commune contenant la méthode abstraite et polymorphe execute().
Plus de problème de stabilité et modularité assuré, comme le code Java qui suit et qui illustre cette méthode le démontre de façon convaincante. Nous limitons ce code à la fonctionnalité d'un des boutons et d'un des menus
//Tout d'abord définitions des fonctionnalités de chaque élément de l'interface
//dans une classe à part implémentant l'interface Command
public class BoutonGrisCommand extends Button implements Command{
  Frame f ;

  public BoutonGrisCommand (String label, Frame f){
        super (label):
        this.f = f;
   }
  public void execute(){
       // code
   }
}
public class FileOpenCommand extends MenuItem implements Command{
  public  FileOpenCommand  (String label){
        super (label):
   }

  public void execute(){
       // code
   }
}
// Création des nouveaux objets boutons et menus à partir de ces classes
unOpen = new FileOpenCommand ("Open") ;
unBoutonGris = new BoutonGrisCommand ("gris", this);
// Ajout de l'interface ActionListerner, comme classiquement fait
unOpen .addActionListener(this) ;
unBoutonGris .addActionListener(this) ;
// Nouvelle méthode « actionPerformed ».. C'est la que ce Pattern prend toute sa signification
// Les tests ont disparus et la méthode plus légère
public void actionPerformed(ActionEvent evt){
  Object obj = evt.getSource( );
   if (obj instanceof Command){
      ((Command)obj).execute();
   }
....
}

Le pattern décorateur, dans l'exemple du programme du flipper, à l'état actuel le flipper contient un ensemble prédéfini d'obstacles.Ces catégories sont fixées une fois pour toutes et se singularisent par l'effet de l'obstacle sur la balle. Or imaginons que nous désirons, lors de l'installation des obstacles dans un flipper, apporter plus de souplesse au comportement de ceux-ci. Alors qu'il est obligatoire que tous les obstacles de type champignon fassent rebondir la balle, on souhaiterait que certains, en plus, incrémente le score, que d'autres changent de couleur, que d'autres encore puissent faire les deux, un troisième comportement étant même envisageable, à la demande : que l'obstacle champignon puissent se caractériser, à la carte, par un ensemble de comportement différent, tous issus d'un nombre fini de possibilités de base.

Une première solution extrêmement lourde et naïve est d'imaginer autant de classe (et donc toutes les combinaisons d'héritage qui le permettent) qu'il y aurait de types d'obstacles différents. Une classe serait de type « Point-Couleur », une autre « Couleur-son », une troisième « Point-Son-Couleur »,etc. On pressent aisément la prolifération de toutes ces classes avec l'accroissement du nombre de fonctionnalités de base possibles. Une solution bien plus élégante est fournie par le pattern « décorateur », dont le diagramme de classe suit.
Le pattern décorateur permet d'enchaîner de manière très souple une succession de fonctionnalités à un même objet, en court-circuitant pour ce faire toutes les combinaisons possibles d'héritage.

Le pattern composite, celui-ci est idéal pour créer des structures complexes dans lesquelles, des éléments se trouvent  imbriqués les uns dans les autres ou, comme dans un mille-feuille, empilé les unes sur les autres. De manière générale, un « composite » est un groupe d'objets dans lequel certains objets peuvent en contenir d'autres. Certains objets seront donc plutôt de type groupe (mais pouvant contenir d'autres groupes), comme nos molécules, alors que d'autres seront isolés et singuliers, tout comme nos atomes.

Non seulement un groupe contiendra soit un ensemble d'objets individués, soit d'autres groupes mais, plus important encore, tant les groupes que les objets individués présenterons partiellement un comportement commun. Le diagramme de classe ci-suivant rend bien compte de ce pattern.

Dans ce diagramme, on considère que les molécules, en plus d'être un ensemble d'atomes, peuvent être considérées ici comme un ensemble de molécules. Les trois méthodes installées uniquement dans la superclasse doivent être redéfinies dans les deux sous-classes mais existent bel est bien dans ces deux là dans des versions différentes. Un cas très parlant d'application de ce pattern est  l'oeuvre de certaines structures arborescentes (comme le langage XML), dans lequel chaque noeud de l'arbre pointe vers un ensemble de noeuds jusqu’à atteindre les feuilles de l'arbre.

Le pattern chain of responsability
La responsable a, entre autres responsabilités, celle de répondre à une demande adressée par le client. A son niveau, la méthode gereDemande est abstraite et se trouve redéfinie dans un  ensemble de sous-classes qui, toutes, ont la possibilité soit de gérer cette demande soit, si elles en sont incapables, de déléguer cette gestion à une autre des sous-classes. C'est la raison pour laquelle la superclasse se réfère elle-même par un lien d'agrégation, afin de pouvoir « refiler le travail » à une autre des sous-classes. Chaque sous-classes peut se refiler la demande, jusqu'à aboutir à celle d'entre elles dont la méthode gereDemande est capable de satisfaire le client.

Dans notre pattern chimique composite, si nous demandons à une molécule, composée d'autre molécules, de nous donner sa masse, elle ne peut qu'additionner la sienne à celles qu'elle demande à toute celles dont elle est composée, comme dans le code qui suit : 
public double donneMasse(){
         double masse = 0;
         for (int i = 0 ; i < lesComposants.size() ; i++){
                masse += ((ComposantChimique)lesComposants.elementAt(i)).donneMasse();
         }
     return masse ;
}

Les patterns strategy et state  le pattern « strategy » est une application directe du polymorphisme. Dans ce pattern, différentes sous-classes implémente de manière une même fonctionalité stratégique. Le client n'a pas à se préoccuper de connaître les différentes implémentations ; celles-ci seront finalement choisie au moment de l'exécution du code en fonction du type précis de l'objet interlocuteur implémentant la version de la stratégie souhaité. Ainsi, dans la figure suivante, lorsque le réacteur demande à une réaction de s'exécuter, cette réaction s'effectuera différemment et selon mode propre, en fonction de sa nature définitive.

La manière dont les molécules réagissent et se comportent en général dépend donc de l'état dans lequel elles se trouvent à un moment donné. Créer autant de sous-classes de molécules qu'il y a d'états possible n'est pas une solution au problème, puisqu'une molécule peut changer d'état au cours du temps alors qu'un objet ne peut changer de classe au cours d'une simulation. La solution proposé par le GOF, comme le montre le diagramme de classe de la figure suivante, est d'associer la molécule à une classe EtatMoleculaire qui, elle, possèdera autant de sous-classes qu'il y a d'états possibles, et d'installer à même ces classes et sous-classes tout ce qui permet de modifier le comportement des molécule selon leur état. Il faut donc déléguer toutes les opérations qui dépendent de l'état de la molécule à son objet état courant. Les transitions entre états peuvent se produire dans une classe extérieure Reacteur ou  se faire à même les classes d'état. Ce pattern accopagne comme il se doit toute mise en oeurvre d'un digramme UML d'état-transition.




Aucun commentaire: