• 4 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 30/10/2024

Organisez vos classes avec les design patterns de structure

Dans ce chapitre, vous découvrirez quels avantages vous pouvez tirer de l'utilisation de design patterns de structure dans votre code.

En quoi consistent les design patterns de structure ?

Je suis certain que vous avez déjà fait vos courses dans un supermarché : le choix est vaste.

Quand vous avez terminé, vous apportez vos articles à la caisse et vous payez. Vous échangez avec la personne à la caisse. C'est assez simple. Il s'agit précisément d'un exemple de pattern de structure où une interface simple masque une grande complexité. En tant que client du magasin, vous ne voulez pas vous préoccuper de la commande des produits, de leur livraison, de leur étiquetage ou de la mise en rayon. Vous voulez faire vos courses et rentrer chez vous.

Les patterns de structure correspondent à des manières d'organiser les classes ou les objets de façon à ce qu'ils soient faciles à utiliser. Il peut se passer beaucoup de choses en coulisses, mais en tant qu'utilisateur, vous n'avez pas besoin d'en connaître toute la complexité. Il existe deux types de patterns de structure. Ceux qui organisent les classes et ceux qui organisent les objets. Nous étudierons les deux types. 

En quoi consiste le pattern Adaptateur ?

Ce pattern répond au problème qui survient lorsque vous disposez d'un code qui attend une interface (ensemble de méthodes), mais que l'implémentation que vous comptez utiliser en fournit d’autres. Comment se sortir de cette situation délicate ? Cela peut arriver par exemple, lorsque vous faites appel à une bibliothèque tierce, après avoir déjà codé une partie de votre système. La bibliothèque fait ce que vous voulez, mais ses classes disposent de méthodes dont les noms diffèrent du code que vous avez écrit.

Plusieurs options s'offrent à vous. L'une d'entre elles (la plus complexe/mauvaise) consiste à revenir en arrière et à modifier tout le code existant, afin qu’il puisse utiliser les nouvelles classes et méthodes. L'autre consiste à créer un adaptateur. L'adaptateur présente l'interface attendue. Et ensuite, l'implémentation de chaque méthode de l'adaptateur appellera des classes de la bibliothèque.

Voyons un exemple concret ! Mon portable dispose d'un connecteur HDMI. Mais j’utilise un vieil écran, qui a seulement un connecteur VGA. J'ai besoin d'un adaptateur pour convertir les signaux et travailler en double écran !

Dans notre jeu de cartes, imaginons que nous ayons créé une interface PlayableCard et que notre PlayingCard l'ait implémentée :

interface PlayableCard {

    void flip();

};


class PlayingCard implements PlayableCard {

    bool faceUp;
    
    void flip () {
    
    faceUp = !faceUp;
    
    }

};

Mais un autre développeur de l’équipe a créé une classe CoolCard qui a l’air mieux que notre implémentation. Nous décidons donc d’utiliser celle-ci à la place :

class CoolCard {

    void turnOver() {
        // implémentation cool ici
    }

};

Si nous utilisions cette nouvelle carte, chaque endroit faisant appel à notre opération flip() devrait être remplacé par turnOver(). Cela n'a rien de très difficile dans notre jeu de cartes, mais imaginez cela sur un projet de plus grande ampleur ! Utilisons à la place un adaptateur qui ressemble à une PlayableCard, mais qui se comporte comme une CoolCard :

class PlayingCardAdapter implements PlayableCard {

    CoolCard thisCard;
    
    void flip() {
    
        thisCard.turnOver();
    
    }

};

Avec ce PlayingCardAdapter , nous n'avons pas besoin de modifier tous les appels à flip() ! Nous devons cependant utiliser des objetsPlayingCardAdapter au lieu d'objetsPlayingCard . Mais souvenez-vous, nous disposons d'une Factory qui fait tout cela ! La Factory est donc la seule partie du code qui doit être informée de l'ajout du nouveau concept CoolCard. :honte:

Qu'est-ce que le pattern Composite ?

Il arrive parfois que vous souhaitiez traiter de la même façon un ensemble d'objets et des objets individuels. Prenons l'exemple du tracé d'une forme. Si vous avez une forme simple, mettons un cercle, vous feriez l'appel suivant :

Shape shape = new Circle();
shape.draw();

Vous souhaiteriez également appeler tout un ensemble de formes, en procédant de la même façon :

Shape shapes = new ComplicatedDiagramOfABunchOfShapes();
shapes.draw();

Le design pattern Composite fait cela ! Chaque élément, qu'il s'agisse d'une simple entité ou d'un ensemble, présente la même interface (mêmes méthodes). L'implémentation d’une méthode au niveau de la collection réalisera l’appel de la méthode pour chacun de ses objets.

Dans le chapitre suivant, nous ajouterons plusieurs vues à l'aide d'un autre pattern. Pour préparer cela, nous voulons que le contrôleur traite une ou plusieurs vues comme si elles étaient identiques.

Effectuons cela ensemble :

Nous plaçons toute la gestion de la vue dans une nouvelle classe composite :

public class GameViewables implements GameViewable {

    List<GameViewable> views;

    public GameViewables () {

        views = new ArrayList<GameViewable> ();

    }

    public void addViewable (GameViewable view) {

        views.add(view);

    }

    @Override
    public void showPlayerName(int playerIndex, String playerName) {

        for (GameViewable view : views) {

            view.showPlayerName(playerIndex, playerName);

        }
    }

    // autres overrides de GameViewable ici
    
}

Nous avons remplacé le fait de disposer d’une unique vue par le fait de disposer d’une liste de plusieurs vues. Ensuite, partout où nous demandions une mise à jour dans l’affichage de la vue, nous passons en revue la liste des vues, en demandant à chacune d'actualiser son affichage. Vous remarquerez cependant que nous n'avons pas eu à modifier les noms de tous ces appels de méthodes pour mettre cela en œuvre. Une seule ou plusieurs vues, cela revient au même pour les appelants de la fonction showPlayerName.

En quoi consiste le pattern Décorateur ?

Le pattern Décorateur est similaire au pattern Composite dans le sens où il permet à un groupe d'objets de se comporter comme si les différents objets n'en formaient qu'un seul. Seule différence, les objets gérés par les décorateurs disposent d’une nouvelle fonctionnalité, dont l’objet initial ne disposait pas.

Imaginez que vous commandiez une coupe de glace. La coupe se compose pour l'essentiel d'une boule de glace. Vous la dégustez avec une cuillère. Vous pouvez aussi ajouter des nappages, ou des fruits... En quelque sorte, vous décorez la coupe de glace. Mais vous continuez bien à la manger à l'aide d'une cuillère. L'interface reste donc la même, même si la coupe de glace est devenue plus complexe.

Revenons à notre jeu de cartes. Nous introduirons une interface nommée IPlayer, implémentée par Player. Lorsque le joueur est gagnant, nous souhaiterions afficher quelque chose de spécial, avec son nom. Mais seulement s'il est gagnant. Nous ajoutons donc une classe Décorateur appelée WinningPlayer.

Implémentons-la ensemble :

Tout d'abord, créons une nouvelle implémentation d'une PlayableCard, appelée WinningPlayer :

public class WinningPlayer implements IPlayer {

    IPlayer winner;
    
    public WinningPlayer (IPlayer player) {

        winner = player;
    
    }
    
    
    public String getName() {
    
        return "***** " + winner.getName() + " *****";
    
    }

}

Notez que nous avons encore besoin du Player d'origine (il contient le nom du joueur). Ce nouveau décorateur reprend la méthode getName, en mettant légèrement en évidence son nom, mais il appelle ensuite le joueur d'origine pour s'exécuter normalement. La dernière partie est vraiment importante ! On ne remplace pas la fonctionnalité, mais on l’enrichit (en la décorant). 

En résumé

  • Les patterns de structure correspondent à des façons d'organiser les classes ou les objets pour qu'ils soient faciles à utiliser. 

  • Le pattern Adaptateur modifie l'interface d'une classe pour la rendre compatible.

  • Le pattern Composite permet de traiter identiquement un objet ou une collection d'objets.

  • Le pattern Décorateur permet d'ajouter et de supprimer des comportements supplémentaires au moment de l'exécution.

Dans le chapitre suivant, nous découvrirons la communication entre les objets en étudiant les design patterns comportementaux.

Exemple de certificat de réussite
Exemple de certificat de réussite