SOLDES de printemps

Prototype

Alias : Clone

Intention

Prototype est un patron de conception qui crée de nouveaux objets à partir d’objets existants sans rendre le code dépendant de leur classe.

Patron de conception prototype

Problème

Nous voulons une copie exacte d’un objet. Comment vous y prendriez-vous ? Tout d’abord, vous devez créer un nouvel objet de cette même classe. Ensuite, vous devez parcourir tous les attributs de l’objet d’origine et copier leurs valeurs dans le nouvel objet.

Super ! Mais il y a un hic. Certains objets ne peuvent pas être copiés ainsi, car leurs attributs peuvent être privés et ne pas être visibles en dehors de l’objet.

Ce qui peut mal tourner lorsque l’on construit un objet « depuis l’extérieur »

Copier un objet « depuis l’extérieur » n’est pas toujours possible.

Un autre problème se profile avec l’approche directe. Étant donné que vous devez connaître la classe de l’objet pour le dupliquer, votre code devient dépendant de cette classe. Même si cette dépendance ne vous effraie pas, il y a un autre hic. Parfois, vous ne connaissez que l’interface implémentée par l’objet, mais pas sa classe concrète. Si le paramètre d’une méthode accepte tous les objets qui implémentent une interface donnée, vous pouvez rencontrer des problèmes.

Solution

Le patron de conception prototype délègue le processus de clonage aux objets qui vont être copiés. Il déclare une interface commune pour tous les objets qui pourront être clonés. Cette interface vous permet de cloner un objet sans coupler votre code à la classe de cet objet. En général, une telle interface ne contient qu’une seule méthode clone.

L’implémentation de cette méthode se ressemble dans toutes les classes. Elle crée un objet de la classe actuelle et reporte les valeurs des attributs de l’objet d’origine dans le nouveau. Vous pouvez également copier des attributs privés, car la plupart des langages de programmation autorisent l’accès à des objets d’une même classe.

Un objet qui peut être cloné est appelé un prototype. Lorsque vos objets possèdent des dizaines d’attributs et des centaines de configurations possibles, le clonage est une alternative au sous-classage.

Prototypes préconstruits

Les prototypes préconstruits sont une alternative au sous-classage.

Leur fonctionnement est le suivant : vous créez un ensemble d’objets configurés différemment. Dès que vous avez besoin de l’un de ces objets, vous clonez un prototype plutôt que d’en construire un nouveau.

Analogie

Dans la vie réelle, des prototypes sont utilisés pour exécuter des tests avant de lancer les chaînes de production d’un produit. Dans ce cas toutefois, les prototypes ne font pas partie intégrante de la fabrication, ils ne jouent qu’un rôle passif.

La division cellulaire

La division cellulaire.

Puisque les prototypes industriels ne se clonent pas eux-mêmes, étudions le processus de la mitose d’une cellule (petit rappel de vos cours de biologie). Une fois la division mitotique terminée, une paire de cellules identiques est créée. La cellule originale sert de prototype et joue un rôle actif dans la création de la copie.

Structure

Implémentation de base

La structure du patron de conception prototypeLa structure du patron de conception prototype
  1. L’interface Prototype déclare les méthodes de clonage. Dans la majorité des cas, il n’y a qu’une seule méthode clone.

  2. La classe Prototype Concret implémente la méthode de clonage. En plus de copier les données de l’objet original dans le clone, cette méthode peut gérer des cas particuliers du processus de clonage, comme des objets imbriqués, démêler les dépendances récursives, etc.

  3. Le Client peut produire une copie de n’importe quel objet implémentant l’interface prototype.

Implémentation du registre de prototypes

Le registre de prototypesLe registre de prototypes
  1. Le Registre de Prototypes facilite l’accès aux prototypes fréquemment utilisés. Il stocke un ensemble d’objets préconstruits qui ont déjà été copiés. Le registre de prototypes le plus simple est une table de hachage (hash map) nom → prototype. Si vous voulez de meilleurs critères de recherche qu’un simple nom, construisez une version plus robuste du registre.

Pseudo-code

Dans cet exemple, le Prototype produit des copies exactes d’objets géométriques, sans coupler le code à leurs classes.

La structure du patron de conception prototype dans l’exemple utilisé

Cloner un ensemble d’objets qui appartiennent à une hiérarchie de classes.

Toutes les formes implémentent la même interface, et cette dernière fournit une méthode de clonage. Une sous-classe peut appeler la méthode de clonage de son parent avant de copier les valeurs de ses propres attributs dans l’objet.

// Prototype de base.
abstract class Shape is
    field X: int
    field Y: int
    field color: string

    // Un constructeur normal.
    constructor Shape() is
        // ...

    // Le constructeur du prototype. Un nouvel objet est
    // initialisé avec les valeurs de l’objet existant.
    constructor Shape(source: Shape) is
        this()
        this.X = source.X
        this.Y = source.Y
        this.color = source.color

    // Le traitement de clonage retourne l’une des sous-classes
    // Forme (Shape)
    abstract method clone():Shape


// Prototype concret. La méthode de clonage crée un nouvel objet
// et le passe au constructeur. Il garde une référence à un
// nouveau clone tant que le constructeur n’a pas terminé. Il
// est donc impossible de se retrouver avec un clone incomplet
// et les résultats du clonage sont toujours fiables.
class Rectangle extends Shape is
    field width: int
    field height: int

    constructor Rectangle(source: Rectangle) is
        // Un appel au constructeur parent est requis pour
        // copier les attributs privés définis dans la classe
        // mère.
        super(source)
        this.width = source.width
        this.height = source.height

    method clone():Shape is
        return new Rectangle(this)


class Circle extends Shape is
    field radius: int

    constructor Circle(source: Circle) is
        super(source)
        this.radius = source.radius

    method clone():Shape is
        return new Circle(this)


// Quelque part dans le code client.
class Application is
    field shapes: array of Shape

    constructor Application() is
        Circle circle = new Circle()
        circle.X = 10
        circle.Y = 10
        circle.radius = 20
        shapes.add(circle)

        Circle anotherCircle = circle.clone()
        shapes.add(anotherCircle)
        // La variable `autreCercle` contient la copie exacte de
        // l’objet `Cercle`.

        Rectangle rectangle = new Rectangle()
        rectangle.width = 10
        rectangle.height = 20
        shapes.add(rectangle)

    method businessLogic() is
        // Le prototype est très pratique, car il vous permet de
        // créer une copie d’un objet en ignorant tout de son
        // type.
        Array shapesCopy = new Array of Shapes.

        // Par exemple, nous ne savons pas exactement quels
        // éléments figurent dans le tableau des formes. Nous
        // savons juste que ce sont tous des formes. Grâce au
        // polymorphisme, lorsque nous appelons la méthode
        // `clone` sur une forme, le programme vérifie sa classe
        // et lance la méthode clone appropriée de cette classe.
        // C’est pour cela que nous fabriquons des clones,
        // plutôt qu’un ensemble d’objets forme.
        foreach (s in shapes) do
            shapesCopy.add(s.clone())

        // Le tableau `CopiesFormes` (shapesCopy) contient les
        // copies exactes du tableau des `formes`.

Possibilités d’application

Utilisez le prototype si vous ne voulez pas que votre code dépende des classes concrètes des objets que vous clonez.

Vous rencontrerez ce cas si votre code manipule des objets obtenus via l’interface d’une application externe. Il arrive que votre code ne puisse pas se baser sur les classes concrètes de ces objets car elles sont inconnues.

Le prototype procure une interface générale au code client et lui permet de travailler avec tous les objets clonables. Cette interface rend le code client indépendant des classes concrètes des objets qu’il clone.

Utilisez ce patron si vous voulez réduire le nombre de sous-classes quand elles ne diffèrent que dans leur manière d’initialiser leurs objets respectifs. Ces sous-classes pourraient avoir été prévues pour créer des objets similaires dans des configurations spécifiques.

Le patron de conception prototype vous permet d’utiliser un ensemble d’objets préconstruits (des prototypes) de différentes manières.

Plutôt que d’instancier une sous-classe qui correspond à une configuration précise, le client peut se contenter de chercher le prototype approprié et de le cloner.

Mise en œuvre

  1. Créez l’interface du prototype et déclarez-y la méthode clone, mais si vous avez une hiérarchie de classes existante, ajoutez juste la méthode à toutes ses classes.

  2. Une classe prototype doit définir un autre constructeur capable de prendre un objet de cette classe comme paramètre. Le constructeur doit copier les valeurs des attributs définis dans la classe de l’objet dans l’instance qui vient d’être créée. Si une sous-classe est concernée, vous devez appeler le constructeur du parent pour gérer le clonage de ses attributs privés.

    Si le langage de programmation que vous utilisez ne gère pas la surcharge, vous pouvez définir une méthode spéciale pour copier les données de l’objet. Faire tout ceci à l’intérieur du constructeur est plus pratique, car il retourne le résultat directement après avoir appelé l’opérateur new.

  3. La méthode de clonage ne contient qu’une seule ligne de code : un opérateur new avec la version prototype du constructeur. Notez bien que chaque classe doit redéfinir la méthode de clonage et utiliser le nom de sa propre classe avec l’opérateur new. Si vous ne le faites pas, la méthode de clonage sera capable de produire des objets de la classe mère.

  4. Vous pouvez également créer un registre centralisé des prototypes pour garder un catalogue de prototypes fréquemment utilisés.

    Ce registre peut être implémenté avec une classe fabrique, ou mis dans une classe prototype de base avec une méthode statique qui récupère le prototype. Cette méthode ira chercher un prototype en fonction du critère de recherche que le code client passe à la méthode. Ce critère peut être une chaîne de caractères ou un ensemble de paramètres de recherche. Dès que le prototype recherché est trouvé, le registre crée un clone et le retourne au client.

    Pour finir, remplacez tous les appels directs aux constructeurs des sous-classes par des appels à la méthode fabrique du registre de prototypes.

Avantages et inconvénients

  • Vous pouvez cloner les objets sans les coupler avec leurs classes concrètes.
  • Vous clonez des prototypes préconstruits et vous pouvez vous débarrasser du code d’initialisation redondant.
  • Vous pouvez créer des objets complexes plus facilement.
  • Vous obtenez une alternative à l’héritage pour gérer les modèles de configuration d’objets complexes.
  • Cloner des objets complexes dotés de références circulaires peut se révéler très difficile.

Liens avec les autres patrons

  • La Fabrique est souvent utilisée dès le début de la conception (moins compliquée et plus personnalisée grâce aux sous-classes) et évolue vers la Fabrique abstraite, le Prototype, ou le Monteur (ce dernier étant plus flexible, mais plus compliqué).

  • Les classes Fabrique abstraite sont souvent basées sur un ensemble de Fabriques, mais vous pouvez également utiliser le Prototype pour écrire leurs méthodes.

  • Le Prototype se révèle utile lorsque vous voulez sauvegarder des copies de Commandes dans l’historique.

  • Les conceptions qui reposent énormément sur le Composite et le Décorateur tirent des avantages à utiliser le Prototype. Il vous permet de cloner les structures complexes plutôt que de les reconstruire à partir de rien.

  • Le Prototype n’est pas basé sur l’héritage, il n’a donc pas ses désavantages. Mais le prototype requiert une initialisation compliquée pour l’objet cloné. La Fabrique est basée sur l’héritage, mais n’a pas besoin d’une étape d’initialisation.

  • Parfois le Prototype peut venir remplacer le Mémento et proposer une solution plus simple. Cela n’est possible que si l’objet (l’état que vous voulez stocker dans l’historique) est assez direct et ne possède pas de liens vers des ressources externes, ou que les liens sont faciles à recréer.

  • Les Fabriques abstraites, Monteurs et Prototypes peuvent tous être implémentés comme des Singletons.

Exemples de code

Prototype en C# Prototype en C++ Prototype en Go Prototype en Java Prototype en PHP Prototype en Python Prototype en Ruby Prototype en Rust Prototype en Swift Prototype en TypeScript