SOLDES de printemps

Mémento

Alias : Jeton, Memento

Intention

Mémento est un patron de conception comportemental qui permet de sauvegarder et de rétablir l’état précédent d’un objet sans révéler les détails de son implémentation.

Patron de conception mémento

Problème

Imaginez que vous êtes en train de créer un éditeur de texte. En plus de pouvoir écrire du texte, votre éditeur permet de le formater, d’insérer des images alignées, etc.

Au bout d’un moment, vous décidez d’ajouter une fonctionnalité qui permet aux utilisateurs d’annuler une action sur le texte. Cette fonctionnalité est devenue si classique au fil des années, que les utilisateurs s’attendent systématiquement à en disposer. Vous choisissez une approche directe pour la mettre en place. Avant d’effectuer une action, l’application enregistre l’état de tous les objets et les sauvegarde. Plus tard, lorsqu’un utilisateur décide d’annuler une action, l’application récupère la dernière sauvegarde de l’historique et s’en sert pour restaurer l’état de tous les objets.

Annuler des actions effectuées dans l’éditeur

Avant d’effectuer une action, l’application prend un instantané (snapshot) du dernier état des objets. Il peut être utilisé plus tard pour rétablir les objets dans leur ancien état.

Prenons un moment pour réfléchir à ces photos. Comment va-t-on s’y prendre pour les créer ? Vous allez probablement devoir parcourir tous les attributs d’un objet et copier leurs valeurs quelque part. Cependant, cela ne fonctionnera que si le contenu de l’objet ne possède pas trop de restrictions. Malheureusement, la plupart des objets ne se laissent pas approcher si facilement et cachent les données importantes dans des attributs privés.

Laissons de côté ce problème pour le moment et considérons que nos objets se comportent en bons hippies : ils préfèrent avoir des relations ouvertes et garder leur état public. Même si cette approche nous permet de produire des instantanés de l’état des objets à volonté, de sérieux problèmes demeurent. Dans le futur, vous pourriez décider de refactoriser certaines classes de l’éditeur, ou d’ajouter ou de supprimer certains attributs. Cela semble facile à première vue, mais vous allez devoir également modifier les classes qui ont la responsabilité de créer la copie de l’état des objets concernés.

Comment faire une copie de l’état privé d’un objet ?

Comment faire une copie de l’état privé d’un objet ?

Mais ce n’est pas tout ! Regardons un peu du côté des « photos » de l’état de l’éditeur. Quel genre de données contiennent-elles ? On doit au moins avoir accès aux coordonnées du curseur de la souris, à la position de la barre de défilement, etc. Pour prendre une photo, vous devez récupérer ces valeurs et les mettre dans un conteneur.

Vous allez très certainement stocker un paquet de ces conteneurs dans une liste qui représente l’historique. Ces conteneurs seront probablement les objets d’une classe. Cette classe possèdera très peu de méthodes, mais aura beaucoup d’attributs qui répliquent l’état de l’éditeur. Pour permettre à d’autres objets de lire et d’écrire les données d’une photo, vous allez devoir rendre ses attributs publics, ce qui exposerait les états de l’éditeur, qu’ils soient privés ou non. Les autres classes deviendraient dépendantes au moindre changement apporté à la classe photo. Si nous rendons ses attributs et méthodes privés, ces changements ne seraient de toute façon pas répercutés sur les autres classes.

Il semblerait que nous sommes dans une impasse : soit on expose tous les détails d’une classe, ce qui les rend trop fragiles, soit on restreint l’accès à leur état, ce qui empêche de prendre des instantanés. Dispose-t-on d’une autre technique pour implémenter un « annuler » ?

Solution

Tous les problèmes que nous rencontrons sont provoqués par une mauvaise encapsulation. Certains objets essayent d’en faire plus que ce qu’ils sont supposés faire. Pour récupérer les données dont ils ont besoin pour effectuer une action, ils envahissent l’espace personnel des autres objets, plutôt que de leur demander d’exécuter l’action eux-mêmes.

Le mémento délègue la création des états des photos à leur propriétaire, l’objet créateur (originator). Plutôt que d’essayer de copier l’état de l’éditeur depuis « l’extérieur », la classe de l’éditeur de texte peut prendre la photo elle-même, car elle a accès à son propre état.

Ce patron propose de stocker la copie de l’état de l’objet dans un objet spécial appelé mémento. Son contenu n’est accessible que pour l’objet qui l’a créé. Les autres objets peuvent communiquer avec les mémentos via une interface limitée qui leur permet de récupérer certaines métadonnées de la photo (date de création, nom de l’action effectuée, etc.), mais pas l’état de l’objet original contenu dans la photo.

Le créateur a un accès total au mémento, alors que le gardien ne peut que consulter les métadonnées

Le créateur a un accès total au mémento, alors que le gardien ne peut que consulter les métadonnées.

Une telle stratégie vous permet de stocker des mémentos à l’intérieur d’autres objets que l’on appelle gardiens (caretakers). Le gardien ne manipule le mémento qu’en passant par l’interface limitée. Il ne peut donc pas modifier les états qui y sont stockés. De son côté, le créateur rétablit les états qu’il désire, car il a accès à tous les attributs du mémento.

Dans l’exemple de notre éditeur de texte, nous pouvons créer une classe historique séparée qui prend le rôle du gardien. La pile de mémentos stockée dans le gardien va grandir chaque fois que l’éditeur va effectuer une action. Vous pouvez même afficher cette pile dans l’interface utilisateur, afin de montrer les dernières opérations effectuées par un utilisateur.

Lorsqu’un utilisateur veut annuler une action, l’historique prend le mémento le plus récent de la pile et l’envoie à l’éditeur en demandant un rollback. Comme l’éditeur possède un accès total au mémento, il change lui-même son état en copiant les valeurs du mémento.

Structure

Implémentation basée sur des classes imbriquées

L’implémentation classique du patron repose sur le principe des classes imbriquées, disponibles dans de nombreux langages de programmation populaires (comme le C++, C# et Java).

Mémento basé sur les classes imbriquéesMémento basé sur les classes imbriquées
  1. La classe Créateur (Originator) peut prendre des instantanés de son propre état, et le restaurer à volonté.

  2. Le Mémento est un objet de valeur qui joue le rôle d’un instantané d’un état du créateur. En général, le mémento n’est pas modifiable et écrit ses données une seule fois dans son constructeur.

  3. Le Gardien sait « quand » et « pourquoi » il doit photographier l’état du créateur, mais il sait également quand l’état doit être restauré.

    Le gardien peut conserver la trace de l’historique du créateur en enregistrant une pile de mémentos. Lorsque le créateur veut revenir dans le passé, le gardien récupère l’élément du haut de la pile et l’envoie à la méthode de restauration du créateur.

  4. Dans cette implémentation, la classe mémento est imbriquée à l’intérieur du créateur. Ceci permet au créateur d’accéder aux attributs et méthodes du mémento, même s’ils sont privés. Le gardien quant à lui n’a qu’un accès limité aux attributs et méthodes du mémento : on le laisse entasser les mémentos dans la pile, mais il ne peut pas les modifier.

Implémentation basée sur une interface intermédiaire

Voici une autre possibilité très pratique pour les langages qui ne gèrent pas les classes imbriquées (oui PHP, je te regarde).

Le mémento sans classes imbriquéesLe mémento sans classes imbriquées
  1. En l’absence de classes imbriquées, vous pouvez restreindre l’accès aux attributs du mémento en établissant une convention : les gardiens ne vont pouvoir manipuler un mémento qu’à travers une interface intermédiaire déclarée explicitement. Cette interface ne déclare que des méthodes liées aux métadonnées du mémento.

  2. De leur côté, les créateurs peuvent manipuler directement les objets mémento, accéder à leurs attributs et méthodes déclarés dans la classe mémento. L’inconvénient de cette technique est que vous devez déclarer tous les membres du mémento en public.

Implémentation avec une encapsulation encore plus stricte

Si vous ne voulez vraiment pas que les autres classes accèdent au créateur en passant par le mémento, vous pouvez vous y prendre différemment.

Le mémento avec une encapsulation stricteLe mémento avec une encapsulation stricte
  1. Cette implémentation permet de gérer plusieurs créateurs et plusieurs mémentos. Chaque créateur manipule sa propre classe mémento. Les créateurs et les mémentos n’exposent pas leur état aux autres.

  2. Il est désormais explicité que les gardiens ne peuvent pas modifier l’état stocké dans le mémento. De plus, la classe gardien n’est plus couplée avec le créateur, car la méthode de restauration est maintenant définie dans la classe mémento.

  3. Chaque mémento devient lié au créateur qui l’a produite. Le créateur s’envoie lui-même et toutes les valeurs de son état au constructeur du mémento. Grâce à l’étroite proximité de ces deux classes, un mémento peut restaurer l’état de son créateur, tant que ce dernier a bien défini les setters appropriés.

Pseudo-code

Cet exemple utilise le patron Commande en plus du mémento pour stocker les photos de l’état de l’éditeur de texte complexe, et rétablit un état précédent lorsqu’on le lui demande.

Structure de l’exemple utilisé pour le mémento

Sauvegarder des photos de l’état de l’éditeur de texte.

Les objets commande prennent le rôle de gardiens. Ils vont chercher le mémento de l’éditeur avant de lancer les actions déclenchées par les commandes. Lorsqu’un utilisateur veut annuler l’action la plus récente, l’éditeur peut se servir du mémento stocké dans cette commande pour revenir à son état précédent.

La classe mémento ne déclare aucun attribut public, ni de getters et de setters. Aucun objet ne peut modifier son contenu. Les mémentos sont reliés à l’objet éditeur qui les a créés. Le mémento peut aussi restaurer l’état de l’éditeur qui lui est associé, en passant les données dans les setters de l’objet éditeur. Comme les mémentos sont liés à des objets éditeur spécifiques, votre application peut gérer plusieurs fenêtres avec des éditeurs indépendants et une pile centralisée d’états à rétablir.

// Le créateur conserve des données importantes qui varient
// parfois avec le temps. Il définit également une méthode pour
// sauvegarder son état dans un mémento, et une autre méthode
// pour rétablir cet état.
class Editor is
    private field text, curX, curY, selectionWidth

    method setText(text) is
        this.text = text

    method setCursor(x, y) is
        this.curX = x
        this.curY = y

    method setSelectionWidth(width) is
        this.selectionWidth = width

    // Sauvegarde l’état actuel dans un mémento.
    method createSnapshot():Snapshot is
        // Le mémento est un objet non modifiable. C’est pour
        // cela que le créateur passe son état dans les
        // paramètres du constructeur du mémento.
        return new Snapshot(this, text, curX, curY, selectionWidth)

// La classe mémento conserve un ancien état de l’éditeur.
class Snapshot is
    private field editor: Editor
    private field text, curX, curY, selectionWidth

    constructor Snapshot(editor, text, curX, curY, selectionWidth) is
        this.editor = editor
        this.text = text
        this.curX = x
        this.curY = y
        this.selectionWidth = selectionWidth

    // Un objet mémento peut être utilisé pour rétablir un
    // ancien état de l’éditeur.
    method restore() is
        editor.setText(text)
        editor.setCursor(curX, curY)
        editor.setSelectionWidth(selectionWidth)

// Un objet commande peut prendre le rôle du gardien. Dans ce
// cas, un mémento est affecté à la commande juste avant que
// l'état du créateur ne change. Lorsqu’une demande de
// restauration arrive, il rétablit l’état du créateur à partir
// du mémento.
class Command is
    private field backup: Snapshot

    method makeBackup() is
        backup = editor.createSnapshot()

    method undo() is
        if (backup != null)
            backup.restore()
    // ...

Possibilités d’application

Utilisez le mémento si vous voulez prendre des photos de l’état d’un objet afin de pouvoir rétablir un de ses états précédents.

Le mémento vous permet de faire des copies complètes de l’état d’un objet (attributs privés inclus), et les stocke en dehors de l’objet. Ce patron est surtout connu pour l’utilisation de la fonctionnalité annuler, mais est également indispensable pour les transactions (par exemple, pour permettre le rollback d’une opération en erreur).

Utilisez ce patron lorsque vous ne pouvez pas accéder directement aux attributs/getters/setters d’un objet sans enfreindre les principes de l’encapsulation.

Le mémento rend l’objet responsable de lui-même, ce qui sécurise les données de l’état de l’objet. Aucun autre objet ne peut lire les données de la photo, l’objet original est donc complètement sécurisé.

Mise en œuvre

  1. Déterminez la classe qui jouera le rôle du créateur. Il est important de savoir si le programme va utiliser un seul objet central ou plusieurs petits objets.

  2. Créez la classe mémento. Déclarez un à un l’ensemble des attributs qui recopient les attributs de la classe créateur.

  3. Rendez la classe mémento non modifiable. Un mémento ne doit écrire ses données qu’une seule fois via son constructeur. Sa classe ne doit pas avoir de setters.

  4. Si votre langage de programmation accepte les classes imbriquées, mettez le mémento à l’intérieur du créateur. Sinon, extrayez une interface vide depuis la classe du mémento et obligez les autres objets à utiliser cette interface pour y accéder. Vous pouvez ajouter des opérations sur les métadonnées dans l’interface, mais rien qui ne puisse exposer l’état du créateur.

  5. Ajoutez une méthode qui crée des mémentos à la classe créateur. Le créateur doit passer son état dans un ou plusieurs paramètres du constructeur du mémento.

    Le type de la valeur de retour de la méthode doit être l’interface extraite dans l’étape précédente (si vous avez suivi cette étape). La méthode qui crée le mémento doit directement manipuler la classe mémento.

  6. Écrivez la méthode qui permet de rétablir l’état du créateur dans sa propre classe. Il doit prendre un objet mémento en paramètre. Si vous avez extrait une interface lors de l’une des étapes précédentes, c’est le type de la valeur de retour que cette méthode doit accepter. Dans ce cas, vous devez caster cet objet dans la classe mémento, car le créateur a besoin d’un accès total à cet objet.

  7. Le gardien, que ce soit une commande, un historique ou n’importe quoi d’autre, doit savoir quand demander de nouveaux mémentos au créateur, comment les stocker et quand restaurer le créateur à l’aide d’un mémento donné.

  8. Le lien entre les gardiens et les créateurs peut être déplacé dans la classe mémento. Dans ce cas, chaque mémento doit être associé à son propre créateur. La méthode de restauration doit également être déplacée dans la classe mémento. Mais faites-le uniquement si la classe mémento est imbriquée dans son créateur, ou si la classe du créateur fournit assez de setters pour redéfinir son état.

Avantages et inconvénients

  • Vous pouvez prendre des instantanés de l’état d’un objet tout en respectant son encapsulation.
  • Vous pouvez simplifier le code du créateur en laissant le gardien gérer l’historique de l’état du créateur.
  • L’application pourrait consommer beaucoup de RAM si les clients créent des mémentos trop souvent.
  • Les gardiens doivent garder la trace du cycle de vie des créateurs pour pouvoir détruire les mémentos obsolètes.
  • La majorité des langages de programmation dynamiques comme le PHP, Python ou le JavaScript ne peuvent pas garantir que l’état sauvegardé dans le mémento ne sera pas modifié.

Liens avec les autres patrons

  • Vous pouvez utiliser la Commande et le Mémento ensemble pour implémenter la fonctionnalité « annuler ». Dans ce cas, les commandes ont la responsabilité d’exécuter les divers traitements sur un objet cible. Les mémentos sauvegardent l’état de cet objet juste avant le lancement de la commande.

  • Vous pouvez utiliser le Mémento avec l’Itérateur pour récupérer l’état actuel de l’itération et rétablir sa valeur plus tard si besoin.

  • 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.

Exemples de code

Mémento en C# Mémento en C++ Mémento en Go Mémento en Java Mémento en PHP Mémento en Python Mémento en Ruby Mémento en Rust Mémento en Swift Mémento en TypeScript