Aller au contenu

Design Pattern Prototype

Créer un objet en copiant un objet existant

Introduction

Le design pattern Prototype est un pattern de création dont l’objectif est de créer de nouveaux objets en copiant des objets existants, appelés prototypes. Lorsqu’un objet est complexe à construire ou nécessite des traitements pour initialiser ses propriétés, créer plusieurs objets similaires peut conduire à exécuter plusieurs fois une partie du code d’initialisation. Cette exécution multiple peut devenir problématique lorsque seules quelques propriétés diffèrent d’un objet à l’autre.

Le design pattern Prototype propose de créer un premier objet servant de modèle, puis de créer de nouveaux objets en le copiant. Chaque copie peut ensuite être modifiée indépendamment.

Problématique

Nous développons une application qui génère des rapports. Un rapport possède un titre et un contenu, ainsi que différentes informations permettant de définir la manière dont ce contenu est mis en forme :

public class Report
{
    public string Title { get; set; }

    public string Content { get; set; }

    public string Header { get; set; }

    public string Footer { get; set; }

    public string FontName { get; set; }

    public int FontSize { get; set; }

    public Report(
        string title,
        string content,
        string header,
        string footer,
        string fontName,
        int fontSize)
    {
        Title = title;
        Content = content;
        Header = header;
        Footer = footer;
        FontName = fontName;
        FontSize = fontSize;
    }
}

L’application doit créer plusieurs rapports utilisant la même configuration :

var salesReport = new Report(
    "Rapport d'activité",
    "...",
    "AlgoWin",
    "Confidentiel",
    "Arial",
    12);

var financialReport = new Report(
    "Rapport financier",
    "...",
    "AlgoWin",
    "Confidentiel",
    "Arial",
    12);

Dans cet exemple, plusieurs propriétés sont identiques. Pourtant, chaque nouveau rapport nécessite de répéter leur initialisation. Le code permettant d’initialiser les propriétés communes est donc dupliqué.

Une solution consiste à créer un rapport correctement configuré, puis à l’utiliser comme modèle, ou prototype, pour créer les rapports suivants.

Implémentation du design pattern Prototype

Une première approche consiste à définir une interface représentant un objet pouvant être cloné :

public interface IPrototype<T>
{
    T Clone();
}

La classe Report implémente cette interface :

public class Report : IPrototype<Report>
{
    public string Title { get; set; }

    public string Content { get; set; }

    public string Header { get; set; }

    public string Footer { get; set; }

    public string FontName { get; set; }

    public int FontSize { get; set; }

    public Report(
        string title,
        string content,
        string header,
        string footer,
        string fontName,
        int fontSize)
    {
        Title = title;
        Content = content;
        Header = header;
        Footer = footer;
        FontName = fontName;
        FontSize = fontSize;
    }

    public Report Clone()
    {
        return new Report(Title, Content, Header, Footer, FontName, FontSize);
    }
}

Le code client peut alors créer et configurer un prototype :

var reportPrototype = new Report(
    string.Empty,
    string.Empty,
    "AlgoWin",
    "Confidentiel",
    "Arial",
    12);

Puis créer différents rapports à partir de celui-ci :

var salesReport = reportPrototype.Clone();
salesReport.Title = "Rapport d'activité";
salesReport.Content = "...";

var financialReport = reportPrototype.Clone();
financialReport.Title = "Rapport financier";
financialReport.Content = "...";

L’initialisation commune des propriétés n’est désormais réalisée qu’une seule fois, lors de la création du prototype. Le code client ne reconstruit plus entièrement chaque rapport : il demande au prototype de produire une copie, puis modifie uniquement les informations spécifiques au nouvel objet.

Structure du design pattern

Le design pattern Prototype contient les éléments suivants :

  • Le Prototype, qui définit l’opération permettant de copier un objet.
  • Le Concrete Prototype, qui implémente cette opération et définit la manière dont l’objet est copié.
  • Le Client, qui crée de nouveaux objets en demandant à un prototype de se copier.

Dans cette implémentation, la responsabilité de la copie appartient à l’objet lui-même. Celui-ci connaît sa structure et peut donc déterminer comment ses différentes propriétés doivent être copiées.

Copie superficielle et copie profonde

L’implémentation du pattern Prototype nécessite une attention particulière lorsque l’objet contient des références vers d’autres objets. Imaginons que la mise en forme soit maintenant représentée par la classe ReportStyle :

public class ReportStyle
{
    public string FontName { get; set; }

    public int FontSize { get; set; }

    public ReportStyle(string fontName, int fontSize)
    {
        FontName = fontName;
        FontSize = fontSize;
    }
}

Modifions la classe Report afin qu’elle possède une propriété de ce type :

public class Report
{
    public string Title { get; set; }

    public ReportStyle Style { get; set; }

    public Report(string title, ReportStyle style)
    {
        Title = title;
        Style = style;
    }
}

Il existe alors deux stratégies principales pour réaliser la copie.

Copie superficielle

Une copie superficielle, aussi appelée shallow copy, consiste à créer un nouvel objet en conservant les références vers les mêmes objets imbriqués :

public Report Clone()
{
    return new Report(Title, Style);
}

Après la copie, les deux rapports sont différents, mais leur propriété Style référence la même instance de ReportStyle.

Copie superficielle : le prototype et sa copie partagent la même instance de ReportStyle

Ainsi, une modification réalisée depuis l’une des copies affecte également l’autre :

var reportPrototype = new Report(
    "Rapport",
    new ReportStyle("Arial", 12));

var salesReport = reportPrototype.Clone();

salesReport.Style.FontSize = 16;

Copie profonde

Une copie profonde, aussi appelée deep copy, consiste également à copier les objets référencés.

Copie profonde : le prototype et sa copie possèdent chacun leur instance de ReportStyle

Ainsi, chaque rapport possède sa propre instance de la classe ReportStyle. L’implémentation de la méthode Clone pourrait être la suivante :

public Report Clone()
{
    return new Report(
        Title,
        new ReportStyle(Style.FontName, Style.FontSize));
}

Modifier le style de la copie n’a désormais aucun impact sur le prototype.

Le choix entre copie superficielle et copie profonde dépend donc du comportement recherché. Cette distinction est particulièrement importante lors de l’utilisation du pattern Prototype avec des graphes d’objets contenant des types référence mutables.

Utilisation de la méthode MemberwiseClone

La classe Object propose la méthode MemberwiseClone, permettant de créer une copie superficielle de l’objet courant. La méthode Clone peut ainsi être implémentée de manière simplifiée :

public Report Clone()
{
    return (Report)MemberwiseClone();
}

Cette méthode copie les champs de l’objet. Pour les types valeur, les valeurs sont copiées. Pour les types référence, les références sont copiées : les objets référencés restent donc partagés entre l’original et sa copie.

La méthode MemberwiseClone réalise une copie superficielle. Si une copie profonde est nécessaire, les objets référencés concernés doivent également être copiés.

Avantages

Le design pattern Prototype permet notamment :

  • De créer un objet à partir d’un objet déjà configuré.
  • De réduire la duplication du code d’initialisation lorsque plusieurs objets possèdent des propriétés communes.
  • D’encapsuler la logique nécessaire à la copie dans la classe concernée.

Inconvénients

L’utilisation du design pattern Prototype peut rendre l’implémentation de la copie complexe lorsque l’objet contient un graphe important d’objets. Il est notamment nécessaire de déterminer les références qui peuvent être partagées et celles qui doivent être copiées.

Une copie profonde peut également avoir un coût lorsque les objets manipulés sont volumineux. Enfin, lorsque la logique de copie est implémentée manuellement, l’ajout d’une nouvelle propriété peut nécessiter de modifier l’implémentation de la méthode de copie.

Les design patterns Prototype et Builder

Les design patterns Prototype et Builder sont tous les deux des patterns de création, mais ils répondent à des problématiques différentes.

Le design pattern Builder permet de construire progressivement un nouvel objet en contrôlant les différentes étapes de sa création. Le design pattern Prototype permet de créer un nouvel objet à partir de la copie d’un objet existant.