Accueil / Tutoriels / Memento

Memento

Publié le 23 Mai 2025
Conception

Memento (mémento en français) est un design pattern comportemental qui permet de capturer et de stocker l’état interne d’un objet sans enfreindre le principe d’encapsulation et de manière à le restaurer ultérieurement.

Principe

Dans de nombreuses applications, certaines actions modifient très fréquemment l’état d’un objet. Dans ce type de situations, il est crucial de pouvoir revenir à un état antérieur. Par exemple, si un utilisateur fait une erreur, si un événement inattendu survient (crash de l’application), s’il faut un historique des modifications ou une sauvegarde temporaire.

Si notre objet est très complexe avec de nombreux attributs privés, il peut être nécessaire pour l’application de sauvegarder son état. Toutefois, cela doit se faire en gardant de bons principes de conception logicielle. En particulier, il est préférable de ne pas exposer ses getters/setters de tous ses attributs pour préserver l’encapsulation. Conformément au principe de responsabilité unique, l’objet doit rester responsable de la gestion de son état. Ainsi, aucun autre composant ne devrait connaître ou manipuler directement l’état interne de l’objet.

Le pattern memento permet de capturer l’état complet ou partiel d’un objet. Cet état va être conservé dans un instantané (le memento) sans l’exposer au reste de l’application et pourra être restauré plus tard à l’objet initial de manière transparente. Le principe d'encapsulation est respecté, car seul l’objet lui-même connaît la structure de son memento.

Exemples d’utilisation de Memento

Annuler les modifications dans un formulaire complexe

Sur un outil back-office d’un site e-commerce, un utilisateur veut modifier une fiche produit avec de nombreux champs (nom, prix, stock, images, etc…). Avant de valider les changements, l’utilisateur veut annuler ses changements pour revenir à l'affichage initial.

Le formulaire peut contenir différents types de champs (texte, valeurs numériques, sélecteurs d’images, dropdown, etc…). En voulant gérer des sauvegardes “manuellement” on risque de dupliquer toute cette logique dans un composant externe et être tenté d’exposer les getters/setters dans le formulaire.

Ici, on va capturer l’état complet de l’objet Product et créer le Memento contenant toutes les données du produit. L’objet Product reste maître de son propre état, les autres composants n'ont pas connaissance de la structure interne de Product. Cette logique peut être facilement implémenter dans d’autres formulaires.

Système de configuration avec sauvegarde et restauration d’états

Sur une interface de gestion de configuration (tableau de bord sur un CMS par exemple), certains utilisateurs veulent modifier des paramètres pour tester certaines configurations de manière temporaire et pouvoir revenir à une version antérieure.

Avant toute modification, l’état de la configuration actuelle est sauvegardé sous forme de memento. L’utilisateur peut alors modifier librement les paramètres et en cas de besoin restaurer l’état précédent. Une fois de plus, on évite d’exposer ou de manipuler les données internes de l’objet à plusieurs endroits de notre code.

Points d’attentions

Chaque memento stocke l’état complet d’un objet à un instant t. Si l’objet est volumineux et/ou que beaucoup de memento sont créés, cela peut avoir un impact sur la mémoire. Il est recommandé de limiter le nombre de memento conservés en imposant une taille maximale, de sauvegarder uniquement les parties les plus importantes d’un objet et de mettre un en place un système de rotation des memento en supprimant progressivement les plus anciens.

Le pattern Memento est pertinent pour des logiques d’annulation et de restaurations sur des cas critiques et récurrents. En effet, l'implémentation de Memento nécessite de créer au moins trois classes (nous y reviendrons plus tard), ce qui peut être disproportionné pour des usages simples. Dans de nombreux cas, une sauvegarde temporaire dans un objet intermédiaire peut suffire.

Le pattern Memento est bien adapté pour des objets dont l’état est simple à sérialiser ou à cloner. Si un objet dépend de ressources externes par exemple (connexion à une base de données, fichiers, etc…), capturer son état peut devenir complexe. Utiliser Memento uniquement sur des objets dont l’état est simple comme des données métiers (nom, statut, paramètres, etc…).

Implémenter un Memento

Nous allons définir trois entités distinctes. L’Originator qui correspond à l’objet dont on veut sauvegarder et restaurer l’état, le Memento qui est une représentation encapsulée de notre état et le Caretaker qui est un gestionnaire des mementos mais qui n’a pas accès à leurs contenus.

L’originator

Il doit pouvoir créer un memento représentant son état actuel et pouvoir restaurer son état à partir d’un memento. Il conserve un contrôle total sur ce qu’il expose. Le memento doit être créé à l’initiative de l’Originator.

Le Memento

C’est un simple conteneur de données, sans logique métier. Il contient l’état interne de l’objet. Il doit être privé ou au moins ne pas être modifiable depuis l’extérieur et n'expose aucune méthode publique pour accéder directement à ses données. Le memento peut être implémenté en tant que classe interne privée à l’Originator (pour maintenir l’encapsulation) ou comme classe autonome mais dont les attributs sont accessibles seulement pour l’Originator.

Le Caretaker

Il s’agit d’un composant qui demande à l’Originator de créer un memento, qui le conserve (dans une pile, une liste ou une base de données) et qui le restitue à l’Originator quand cela est nécessaire (pour un rollback par exemple). Il ne doit pas manipuler le contenu des mementos.

Exemple d'utilisation de Memento en Go


package usersettings

// Memento Interface ou alias de memento exporté
type Memento any // Alias, seul le package connaît son contenu

// UserSettings Originator
type UserSettings struct {
   theme                string
   language             string
   notificationsEnabled bool
}

func NewUserSettings(theme, language string, notificationsEnabled bool) *UserSettings {
   return &UserSettings{theme, language, notificationsEnabled}
}

func (s *UserSettings) Save() Memento {
   return &settingsMemento{
      theme:                s.theme,
      language:             s.language,
      notificationsEnabled: s.notificationsEnabled,
   }
}

func (s *UserSettings) Restore(m Memento) {
   if mem, ok := m.(*settingsMemento); ok {
      s.theme = mem.theme
      s.language = mem.language
      s.notificationsEnabled = mem.notificationsEnabled
   }
}

func (s *UserSettings) Theme() string              { return s.theme }
func (s *UserSettings) Language() string           { return s.language }
func (s *UserSettings) NotificationsEnabled() bool { return s.notificationsEnabled }

// Memento non exporté
type settingsMemento struct {
   theme                string
   language             string
   notificationsEnabled bool
}



package main

import (
   "fmt"
   "designpatterns/memento/usersettings"
)

type SettingsCaretaker struct {
   memento usersettings.Memento
}

func main() {
   settings := usersettings.NewUserSettings("light", "en", true)
   caretaker := &SettingsCaretaker{}

   // Sauvegarde de l'état initial
   caretaker.memento = settings.Save()

   fmt.Println("Avant modification :")
   printSettings(settings)

   // Modifications de l'utilisateur
   settings.Restore(usersettings.NewUserSettings("dark", "fr", false).Save())

   fmt.Println("Après modification :")
   printSettings(settings)

   // Restauration de l'état précédent
   settings.Restore(caretaker.memento)
   fmt.Println("Après restauration :")
   printSettings(settings)
}

func printSettings(s *usersettings.UserSettings) {
   fmt.Printf("Thème: %s, Langue: %s, Notifications activées: %v\n",
      s.Theme(), s.Language(), s.NotificationsEnabled())
}

Le même exemple en PHP


namespace Practice\DesignPatterns\Memento;

class UserSettings
{
   private string $theme;
   private string $language;
   private bool $notificationsEnabled;

   public function __construct(string $theme, string $language, bool $notificationsEnabled)
   {
       $this->theme = $theme;
       $this->language = $language;
       $this->notificationsEnabled = $notificationsEnabled;
   }

   public function save(): SettingsMemento
   {
       return new SettingsMemento($this->theme, $this->language, $this->notificationsEnabled);
   }

   public function restore(SettingsMemento $memento): void
   {
       $this->theme = $memento->getTheme();
       $this->language = $memento->getLanguage();
       $this->notificationsEnabled = $memento->isNotificationsEnabled();
   }

   public function __toString(): string
   {
       return "Theme: {$this->theme}, Langue: {$this->language}, Notifications: " . ($this->notificationsEnabled ? "activées" : "désactivées");
   }
}


namespace Practice\DesignPatterns\Memento;

class SettingsMemento
{
   public function __construct(
       private string $theme,
       private string $language,
       private bool $notificationsEnabled
   ) {}

   public function getTheme(): string { return $this->theme; }
   public function getLanguage(): string { return $this->language; }
   public function isNotificationsEnabled(): bool { return $this->notificationsEnabled; }
}


namespace Practice\DesignPatterns\Memento;

class SettingsCaretaker
{
   public ?SettingsMemento $memento = null;
}


declare(strict_types=1);

use Practice\DesignPatterns\Memento\SettingsCaretaker;
use Practice\DesignPatterns\Memento\UserSettings;

require "./vendor/autoload.php";

// Exemple d'utilisation
$settings = new UserSettings("light", "en", true);
$caretaker = new SettingsCaretaker();

$caretaker->memento = $settings->save(); // sauvegarde de l'état initial

// Modifications par l'utilisateur
$settings = new UserSettings("dark", "fr", false);
echo "Paramètres modifiés : $settings\n";

// Annulation
$settings->restore($caretaker->memento);
echo "Paramètres restaurés : $settings\n";

Conclusion

J'espère que grâce à cet article vous êtes désormais plus à l'aise avec le design pattern Memento. Vous pouvez retrouver mon article sur les design patterns ou les autres tutoriels sur Bridge, Mediator, Iterator, Facade et Adapter.