Aller au contenu

Patrons de conception I

Lorsque nous écrivons des logiciels, certains défis de conception génériques semblent apparaître encore et encore. Et bien qu’il soit honorable de progressivement inventer votre propre trousse d’outils pour ces défis, il n’est pas nécessaire de réinventer la roue : avec le temps, la communauté du génie logiciel a rassemblé un ensemble de solutions réutilisables, appelées patrons de conception. Bien qu’ils soient réutilisables, il ne s’agit pas simplement de bibliothèques de code fournissant une fonctionnalité précise, mais plutôt de lignes directrices abstraites sur la manière d’organiser les classes et les méthodes de votre code pour mieux répondre à des problèmes de conception récurrents.

Résumé du cours

Dans cette leçon, nous examinerons une sélection de patrons de conception : « Singleton », « Composite » et « Command ».

Motivation

Les patrons de conception sont des macro-patrons réutilisables. Ils servent de modèles pouvant être suivis comme stratégies de conception fiables pour des problèmes récurrents précis.

Tout au long de cette leçon, nous examinerons trois catégories de patrons de conception :

  • Patrons de fabrication (ou patrons de création) : visant la création d’objets.
  • Patrons structurels : visant l’agencement des classes pour structurer l’état.
  • Patrons comportementaux : visant les algorithmes et le comportement des objets.

Patrons de fabrication (Factory)

Les patrons de fabrication (aussi appelés patrons de création) ciblent le processus de création d’objets. Les intérêts communs, selon le « Gang of Four », sont :

  • « [...] encapsuler la connaissance des classes concrètes utilisées par un système. »
  • « [...] cacher comment les instances de ces classes sont créées et assemblées. »

Aujourd’hui, nous ne verrons qu’un seul patron de fabrication pour illustrer ces caractéristiques.

Singleton

Intention

« S’assurer qu’il n’existe qu’une seule entité pour une classe donnée. »

Motivation

Nous voulons stocker les entrées de l’utilisateur dans une base de données en RAM. Pour assurer la cohérence, il ne doit exister qu’un seul objet base de données.

Patron : Cacher le constructeur de la classe et prendre le contrôle de la création des objets avec une méthode d’accès statique.

Diagramme UML

En UML, nous indiquons les méthodes privées avec un -. Notez que le constructeur est privé :

classDiagram
    direction TB

    class Database {
    <<class>>
    - Database()
    + getInstance()*
    }

    Database *--> "0..1"Database: singleton reference

Utilisation

Pour appliquer le patron, nous n’avons besoin que de trois éléments :

  • S’assurer que le constructeur est private.
  • Introduire un champ interne private pour la seule et unique référence du singleton.
  • Offrir une méthode public et static getInstance() qui gère la référence du singleton.

Exemple de code :

class Database {

  private static Database singletonReference = null;

  /**
   * Private constructor, so no one else can create entities.
   */
  private Database() {
    // Do whatever needs to be done to set up database here
    // (not relevant for singleton pattern)
  }

  /**
   * Provides access to the singleton reference.
   * If not yet initialized creates a new Database object. 
   * Otherwise returns the one and only existing object.
   */
  public static Database getInstance() {
    // If not yet initialized, create a first database object.
    if (singletonReference == null) {
      singletonReference = new Database();
    }
    // afterwards / else just return the reference already existing
    return singletonReference;
  }
}

Appliquer le patron n’est que la moitié du travail, il faut encore l’utiliser concrètement. Heureusement, appeler le patron est simple :

  • Il suffit d’appeler getInstance chaque fois que nous avons besoin de l’unique objet Database.
  • Le patron singleton s’assurera que vous recevez toujours la même référence.
Pourquoi la bibliothèque Objenesis est-elle si problématique?

Objenesis utilise la réflexion pour créer de nouveaux objets, même si le constructeur est privé. Ainsi, on peut utiliser Objenesis pour contourner le patron singleton — bien que techniquement possible, ce n’est probablement pas une brillante idée. Les singletons remplissent un rôle précis et doivent être respectés. Imaginez que plusieurs objets base de données se mettent soudainement à exister — nous aurions immédiatement des incohérences difficiles à déboguer!

À propos des singletons et de equals

  • S’il est correctement implémenté, toute référence de singleton obtenue doit pointer vers le même objet :

  • On peut donc en conclure que, contrairement à d’autres objets, la comparaison d’un singleton avec == est sécuritaire.

    • On peut le distinguer de null.
    • On peut le distinguer d’autres objets.
    • On n’a pas besoin de distinguer les objets selon leur contenu.
  • Certains novices (ou programmeurs très paresseux) ont tendance à éviter d’implémenter correctement equals en rendant toutes les classes des singletons.

  • Malheureusement, cela élimine immédiatement l’intérêt de la programmation orientée objet.

Définissez les singletons avec prudence.

Les singletons vont à l’encontre de la philosophie orientée objet, où toute classe peut être instanciée autant de fois qu’on le désire. Ne transformez jamais une classe en singleton simplement pour contourner une difficulté de programmation. Les singletons ont un rôle précis — être trop paresseux pour écrire une implémentation correcte de comparaison d’objets n’en fait pas partie.

Patrons structurels

Jusqu’ici, nous n’avons parlé que de patrons pour la création d’objets. Mais tous les défis de conception ne concernent pas la création. Souvent, ce qui nous préoccupe davantage est l’information qu’un objet doit contenir et la manière optimale de structurer cette information dans des relations de classes et d’objets.

Les patrons structurels s’intéressent à la manière dont les classes et objets sont composés pour former des structures plus vastes.
— Gang of Four, Design Patterns, p.137

Composite

Intention

Le patron composite aide à créer des structures d’objets complexes, permettant de traiter de façon uniforme n’importe quelle partie de ces structures.

Motivation

Un exemple courant : les éléments d’un logiciel de dessin :

  • du texte et des lignes comme structures primitives ;
  • des formes plus complexes constituées de primitives ;
  • possiblement des groupements massifs contenant primitives et formes.

Illustration :

Le résultat : bien que nous ayons différents types d’éléments (primitives, objets, groupements), les opérations que nous effectuons ne dépendent pas réellement de leur type :

  • Déplacer un élément
  • Mettre un élément à l’échelle
  • Ajouter / retirer un élément

Diagramme UML

Le patron composite représente intrinsèquement une structure imbriquée, semblable à un arbre. Chaque nœud (un composant) peut être soit une feuille (primitive), soit un composite (objet / groupe) :

Au final, qu’on traite du texte, d’une ligne, d’un rectangle ou d’un regroupement d’objets : nous pouvons les traiter de manière uniforme en tant que composants de notre dessin et appliquer un ensemble statique d’opérations, comme move, scale, add, …

Quel principe de conception fondamental est violé par la classe Leaf ?

Le principe de substitution de Liskov. Leaf restreint l’ensemble des fonctionnalités fournies par sa superclasse Component. Les feuilles ne peuvent pas avoir d’enfants, donc il est impossible de add, remove ou get des sous-composants.

Utilisation

Pour commencer, nous avons besoin d’une classe représentant le composant composite. Dans notre exemple d’éditeur graphique, il peut s’agir d’une classe abstraite ou d’une interface Graphic :

/**
 * This interface represents the "Graphic" in the composite pattern.
 * It can be implemented / subclassed by primitives or composites.
 */
interface Component {

  void move(int x, int y);

  void add(Component component);
}

Après avoir créé d’autres primitives, nous pouvons construire une structure composite et appliquer des opérations composites, par exemple move.

public static void main(String[] args) {
  // Here we create some sample primitives
  Component line1 = new Line(0, 0, 4, 0);
  Component line2 = new Line(0, 0, 2, 2);
  Component line3 = new Line(2, 2, 4, 0);

  // Here we create a triangle, consisting of "primitives" (lines)
  Component triangle = new Composite();
  triangle.add(line1);
  triangle.add(line2);
  triangle.add(line3);

  // Finally we create a group of triangle and text
  Component text = new Text("TriangleText", 2, 1);
  Component group = new Composite();
  group.add(triangle);
  group.add(text);

  // Moving the group will recursively apply move operation on all (deep) children:
  group.move(2, 3);
}

L’appel final à move est intéressant, car nous l’appliquons sur une structure Composite. Par conséquent, l’opération move doit être propagée récursivement jusqu’à toutes les primitives de la structure de composants.

Quelle est la structure d’objet composite complète utilisée pour propager move ?

Patrons comportementaux

Jusqu’ici, nous avons vu des patrons pour la création d’objets et des patrons structurels pour organiser classes et objets. Dans ce dernier segment, nous jetons un coup d’œil aux patrons liés aux algorithmes et au comportement.

Encore une fois, le « Gang of Four » fournit des pistes utiles :

Les patrons comportementaux s’intéressent aux algorithmes et à l’attribution des responsabilités entre objets. Ils décrivent non seulement des patrons d’objets ou de classes, mais aussi des patrons de communication entre eux. Ces patrons caractérisent des flux de contrôle complexes, difficiles à suivre à l’exécution. Ils vous incitent à déplacer votre attention du flux de contrôle pour vous concentrer sur la manière dont les objets sont interconnectés.

Avez-vous déjà vu des patrons correspondant à certaines de ces descriptions ?

Oui! Nous avons déjà vu deux patrons comportementaux dans ce cours : le patron observateur (« observer pattern ») et le patron de double distribution (« double dispatch »).

Dans cette section, nous examinerons un patron comportemental exemplaire pour illustrer ces caractéristiques.

Commande

Intention

« Encapsuler une requête comme un objet, permettant ainsi de paramétrer les clients avec différentes requêtes, de mettre en file ou de journaliser ces requêtes, et de prendre en charge les opérations annulables. »
— GoF, Design Patterns

Motivation

Le patron Commande devient intéressant lorsqu’il existe un décalage entre le moment où une action est déclenchée et celui où elle est exécutée. Un exemple courant : les menus d’interface utilisateur.

  • Un bouton déclenche un comportement, mais il est rare que vous souhaitiez implémenter la logique directement dans le code UI.
  • L’utilisation d’un objet « commande » dédié permet d’encapsuler l’action attendue pour qu’elle soit exécutée ailleurs.
    • L’objet commande peut être transmis comme n’importe quel autre objet.
    • En fait, le créateur de l’objet peut même ignorer complètement où la commande sera finalement exécutée !
    • Facultativement, la commande concrète peut contenir une référence à un récepteur, sur lequel elle s’exécute quand execute est invoqué.

Diagramme UML

Peu importe quel élément de menu est cliqué : un mécanisme commun s’applique. Un objet commande est créé puis exécuté.

Une série de commandes exécutées permet automatiquement de maintenir un historique et d’offrir des opérations d’annulation/rétablissement (undo/redo).

Crédit d’image : GoF — Design Patterns, p.233.

Utilisation

En suivant le patron Commande, nous pouvons facilement associer des éléments d’interface (p. ex. des boutons de menu) à un comportement :

  • Un élément UI crée une nouvelle commande concrète.
  • La commande incarne en interne un comportement à exécuter via execute().

    • Celui-ci peut être simple, p. ex. déléguer un appel paste() :

    • Ou plus complexe, p. ex. demander à l’utilisateur un nom de document avant d’en créer un nouveau :

Littérature

Pour les esprits curieux, quelques inspirations et lectures complémentaires :

Voici le lien pour passer à l’unité de laboratoire : Lab 08