Modèle de conception pour le moteur d'annulation

117

J'écris un outil de modélisation structurelle pour une application de génie civil. J'ai une énorme classe de modèle représentant l'ensemble du bâtiment, qui comprend des collections de nœuds, d'éléments de ligne, de charges, etc. qui sont également des classes personnalisées.

J'ai déjà codé un moteur d'annulation qui enregistre une copie profonde après chaque modification du modèle. Maintenant, j'ai commencé à me demander si j'aurais pu coder différemment. Au lieu de sauvegarder les copies complètes, je pourrais peut-être enregistrer une liste de chaque action de modificateur avec un modificateur inverse correspondant. Pour que je puisse appliquer les modificateurs inverses au modèle actuel à annuler, ou les modificateurs à refaire.

Je peux imaginer comment vous exécuteriez des commandes simples qui modifient les propriétés des objets, etc. Mais qu'en est-il des commandes complexes? Comme insérer de nouveaux objets de nœud dans le modèle et ajouter des objets de ligne qui conservent des références aux nouveaux nœuds.

Comment procéder pour mettre cela en œuvre?

Ozgur Ozcitak
la source
Si j'ajoute le commentaire "Undo Algorthim", est-ce que cela me permettra de rechercher "Undo Algorithm" et de trouver ceci? C'est ce que j'ai cherché et j'ai trouvé quelque chose de fermé en double.
Peter Turner
hay, je veux aussi développer undo / redo dans l'application que nous développons. Nous utilisons le framework QT4 et avons besoin de nombreuses actions complexes undo / redo .. Je me demandais, avez-vous réussi à utiliser Command-Pattern?
Ashika Umanga Umagiliya
2
@umanga: Cela a fonctionné mais ce n'était pas facile. Le plus dur était de garder une trace des références. Par exemple, lorsqu'un objet Frame est supprimé, ses objets enfants: les nœuds, les charges agissant sur lui et de nombreuses autres affectations utilisateur doivent être conservés pour être réinsérés lorsqu'ils sont annulés. Mais certains de ces objets enfants étaient partagés avec d'autres objets et la logique d'annulation / rétablissement est devenue assez complexe. Si le modèle n'était pas si grand, je conserverais l'approche memento; c'est beaucoup plus facile à mettre en œuvre.
Ozgur Ozcitak
c'est un problème amusant sur lequel travailler, pensez à la façon dont les dépôts de code source le font, comme svn (ils gardent les différences entre les commits).
Alex

Réponses:

88

La plupart des exemples que j'ai vus utilisent une variante du modèle de commande pour cela. Chaque action utilisateur qui peut être annulée obtient sa propre instance de commande avec toutes les informations nécessaires pour exécuter l'action et l'annuler. Vous pouvez ensuite maintenir une liste de toutes les commandes qui ont été exécutées et vous pouvez les annuler une par une.

Mendelt
la source
4
C'est essentiellement ainsi que fonctionne le moteur d'annulation de Cocoa, NSUndoManager.
amrox
33

Je pense que le souvenir et la commande ne sont pas pratiques lorsque vous avez affaire à un modèle de la taille et de la portée que l'OP implique. Ils fonctionneraient, mais ce serait beaucoup de travail à maintenir et à étendre.

Pour ce type de problème, je pense que vous devez intégrer la prise en charge de votre modèle de données pour prendre en charge les points de contrôle différentiels pour chaque objet impliqué dans le modèle. Je l'ai fait une fois et cela a fonctionné très bien. La chose la plus importante à faire est d'éviter l'utilisation directe de pointeurs ou de références dans le modèle.

Chaque référence à un autre objet utilise un identificateur (comme un entier). Chaque fois que l'objet est nécessaire, vous recherchez la définition actuelle de l'objet dans une table. Le tableau contient une liste chaînée pour chaque objet contenant toutes les versions précédentes, ainsi que des informations sur le point de contrôle pour lequel ils étaient actifs.

La mise en œuvre de undo / redo est simple: faites votre action et établissez un nouveau point de contrôle; restaurer toutes les versions d'objet au point de contrôle précédent.

Cela demande une certaine discipline dans le code, mais présente de nombreux avantages: vous n'avez pas besoin de copies complètes puisque vous effectuez un stockage différentiel de l'état du modèle; vous pouvez définir la quantité de mémoire que vous souhaitez utiliser ( très important pour des choses comme les modèles CAO) en fonction du nombre de restaurations ou de la mémoire utilisée; très évolutif et nécessitant peu de maintenance pour les fonctions qui opèrent sur le modèle car elles n'ont rien à faire pour implémenter undo / redo.

Jeff Kotula
la source
1
Si vous utilisez une base de données (par exemple sqlite) comme format de fichier, cela peut être presque automatique
Martin Beckett
4
Si vous augmentez cela en suivant les dépendances introduites par les modifications apportées au modèle, vous pourriez potentiellement avoir un système d'arborescence d'annulation (c'est-à-dire si je change la largeur d'une poutre, puis que je travaille sur un composant séparé, je peux revenir et annuler la poutre change sans perdre les autres éléments). L'interface utilisateur pour cela pourrait être un peu lourde, mais elle serait beaucoup plus puissante qu'une annulation linéaire traditionnelle.
Sumudu Fernando
Pouvez-vous expliquer davantage l'idée de cet identifiant par rapport aux pointeurs? Sûrement un pointeur / adresse mémoire fonctionne aussi bien qu'un id?
paulm
@paulm: essentiellement les données réelles sont indexées par (id, version). Les pointeurs font référence à une version particulière d'un objet, mais vous cherchez à vous référer à l'état actuel d'un objet, quel qu'il soit, donc vous voulez l'adresser par id, pas par (id, version). Vous pouvez le restructurer afin de stocker un pointeur vers la table (version => data) et de choisir la dernière à chaque fois, mais cela a tendance à nuire à la localité lorsque vous persistez des données, trouble un peu les préoccupations et rend plus difficile la tâche faire certaines sortes de requêtes courantes, donc ce n'est pas la façon dont cela serait normalement fait.
Chris Morgan le
17

Si vous parlez de GoF, le modèle Memento traite spécifiquement de l'annulation.

Andy Whitfield
la source
7
Pas vraiment, cela répond à son approche initiale. Il demande une approche alternative. La première étant de stocker l'état complet pour chaque étape tandis que la dernière ne stockant que les "diffs".
Andrei Rînea
15

Comme d'autres l'ont indiqué, le modèle de commande est une méthode très puissante pour implémenter Undo / Redo. Mais il y a un avantage important que je voudrais mentionner au modèle de commande.

Lorsque vous implémentez annuler / rétablir à l'aide du modèle de commande, vous pouvez éviter de grandes quantités de code dupliqué en faisant abstraction (dans une certaine mesure) des opérations effectuées sur les données et en utilisant ces opérations dans le système d'annulation / restauration. Par exemple dans un éditeur de texte couper et coller sont des commandes complémentaires (en dehors de la gestion du presse-papiers). En d'autres termes, l'opération d'annulation pour une coupe est coller et l'opération d'annulation pour une pâte est coupée. Cela s'applique à des opérations beaucoup plus simples comme la saisie et la suppression de texte.

La clé ici est que vous pouvez utiliser votre système d'annulation / rétablissement comme système de commande principal pour votre éditeur. Au lieu d'écrire le système tel que «créer un objet d'annulation, modifier le document», vous pouvez «créer un objet d'annulation, exécuter une opération de rétablissement sur l'objet d'annulation pour modifier le document».

Maintenant, certes, beaucoup de gens se disent "Eh bien, duh, ne fait pas partie du point du modèle de commande?" Oui, mais j'ai vu trop de systèmes de commande qui ont deux ensembles de commandes, un pour les opérations immédiates et un autre pour annuler / rétablir. Je ne dis pas qu'il n'y aura pas de commandes spécifiques aux opérations immédiates et à annuler / rétablir, mais réduire la duplication rendra le code plus maintenable.

Torlack
la source
1
Je n'ai jamais pensé pasteà cut^ -1.
Lenar Hoyt
8

Vous voudrez peut-être vous référer au code Paint.NET pour leur annulation - ils ont un très bon système d'annulation. C'est probablement un peu plus simple que ce dont vous aurez besoin, mais cela pourrait vous donner des idées et des directives.

-Adam

Adam Davis
la source
4
En fait, le code Paint.NET n'est plus disponible, mais vous pouvez obtenir le code fourchu.google.com/p/paint-mono
Igor Brejc
7

Cela pourrait être un cas où la CSLA est applicable. Il a été conçu pour fournir une prise en charge complexe des annulations d'objets dans les applications Windows Forms.

Eric Z Beard
la source
6

J'ai implémenté avec succès des systèmes d'annulation complexes en utilisant le modèle Memento - très facile, et a l'avantage de fournir naturellement un cadre Redo. Un avantage plus subtil est que les actions agrégées peuvent également être contenues dans une seule annulation.

En un mot, vous avez deux piles d'objets souvenirs. Un pour Annuler, l'autre pour Redo. Chaque opération crée un nouveau souvenir, qui sera idéalement des appels pour changer l'état de votre modèle, document (ou autre). Cela est ajouté à la pile d'annulation. Lorsque vous effectuez une opération d'annulation, en plus d'exécuter l'action Annuler sur l'objet Memento pour modifier à nouveau le modèle, vous faites également sortir l'objet de la pile Annuler et le pousser directement sur la pile Rétablir.

La manière dont la méthode pour changer l'état de votre document est implémentée dépend entièrement de votre implémentation. Si vous pouvez simplement faire un appel API (par exemple ChangeColour (r, g, b)), faites-le précéder d'une requête pour obtenir et enregistrer l'état correspondant. Mais le modèle prendra également en charge la création de copies complètes, d'instantanés de mémoire, de création de fichiers temporaires, etc. - tout dépend de vous car il s'agit simplement d'une implémentation de méthode virtuelle.

Pour effectuer des actions d'agrégation (par exemple, l'utilisateur Shift-Sélectionne une charge d'objets sur lesquels effectuer une opération, telle que supprimer, renommer, modifier l'attribut), votre code crée une nouvelle pile d'annulation en tant que souvenir unique et la transmet à l'opération réelle à ajouter les opérations individuelles à. Ainsi, vos méthodes d'action n'ont pas besoin (a) d'avoir une pile globale à se soucier et (b) peuvent être codées de la même façon qu'elles soient exécutées isolément ou dans le cadre d'une opération d'agrégation.

De nombreux systèmes d'annulation sont uniquement en mémoire, mais vous pouvez conserver la pile d'annulation si vous le souhaitez, je suppose.

Greg Whitfield
la source
5

Je viens de lire sur le modèle de commande dans mon livre de développement agile - peut-être que cela a du potentiel?

Vous pouvez demander à chaque commande d'implémenter l'interface de commande (qui a une méthode Execute ()). Si vous souhaitez annuler, vous pouvez ajouter une méthode d'annulation.

plus d'infos ici

Dave Arkell
la source
4

Je suis avec Mendelt Siebenga sur le fait que vous devriez utiliser le modèle de commande. Le motif que vous avez utilisé était le motif Memento, qui peut devenir et deviendra très inutile avec le temps.

Étant donné que vous travaillez sur une application gourmande en mémoire, vous devriez être en mesure de spécifier la quantité de mémoire que le moteur d'annulation est autorisé à utiliser, le nombre de niveaux d'annulation enregistrés ou le stockage sur lequel ils seront conservés. Si vous ne le faites pas, vous serez bientôt confronté à des erreurs résultant du manque de mémoire de la machine.

Je vous conseillerais de vérifier s'il existe un framework qui a déjà créé un modèle pour les annulations dans le langage de programmation / framework de votre choix. C'est bien d'inventer de nouvelles choses, mais il vaut mieux prendre quelque chose de déjà écrit, débogué et testé dans des scénarios réels. Cela aiderait si vous ajoutiez ce que vous écrivez, afin que les gens puissent recommander des cadres qu'ils connaissent.

Omer van Kloeten
la source
3

Projet Codeplex :

C'est un cadre simple pour ajouter la fonctionnalité Annuler / Rétablir à vos applications, basé sur le modèle de conception classique de Command. Il prend en charge les actions de fusion, les transactions imbriquées, l'exécution retardée (exécution sur la validation de transaction de niveau supérieur) et l'historique d'annulation non linéaire possible (où vous pouvez avoir le choix entre plusieurs actions à refaire).

sg7
la source
2

La plupart des exemples que j'ai lus le font en utilisant la commande ou le modèle memento. Mais vous pouvez également le faire sans motifs de conception avec une simple structure de déque .

Patrik Svensson
la source
Que mettriez-vous dans la deque?
Dans mon cas, j'ai mis l'état actuel des opérations pour lesquelles je voulais annuler / rétablir la fonctionnalité. En ayant deux deques (undo / redo), j'annule dans la file d'attente d'annulation (pop premier élément) et je l'insère dans la redo dequeue. Si le nombre d'éléments dans les files d'attente dépasse la taille préférée, je fais apparaître un élément de la queue.
Patrik Svensson
2
Ce que vous décrivez en fait IS un modèle de conception :). Le problème avec cette approche est lorsque votre état prend beaucoup de mémoire - conserver plusieurs dizaines de versions d'état devient alors peu pratique, voire impossible.
Igor Brejc
Ou vous pouvez stocker une paire de fermetures représentant une opération normale et une opération d'annulation.
Xwtek
2

Une manière intelligente de gérer l'annulation, qui rendrait votre logiciel également adapté à la collaboration multi-utilisateurs, consiste à mettre en œuvre une transformation opérationnelle de la structure des données.

Ce concept n'est pas très populaire mais bien défini et utile. Si la définition vous semble trop abstraite, ce projet est un exemple réussi de la façon dont une transformation opérationnelle pour les objets JSON est définie et implémentée en Javascript

Danza
la source
1

Nous avons réutilisé le chargement de fichier et enregistré le code de sérialisation pour les «objets» pour un formulaire pratique pour enregistrer et restaurer l'état entier d'un objet. Nous poussons ces objets sérialisés sur la pile d'annulation - avec des informations sur l'opération qui a été effectuée et des conseils sur l'annulation de cette opération s'il n'y a pas suffisamment d'informations glanées à partir des données sérialisées. Undo and Redoing consiste souvent simplement à remplacer un objet par un autre (en théorie).

Il y a eu BEAUCOUP de bogues dus à des pointeurs (C ++) vers des objets qui n'ont jamais été corrigés lorsque vous exécutez d'étranges séquences de restauration d'annulation (ces endroits ne sont pas mis à jour pour des «identifiants» plus sûrs). Les bugs dans ce domaine sont souvent ... ummm ... intéressants.

Certaines opérations peuvent être des cas particuliers pour l'utilisation de la vitesse / des ressources - comme le dimensionnement des choses, le déplacement des choses.

La multi-sélection fournit également des complications intéressantes. Heureusement, nous avions déjà un concept de regroupement dans le code. Le commentaire de Kristopher Johnson sur les sous-éléments est assez proche de ce que nous faisons.

Aardvark
la source
Cela semble de plus en plus impraticable à mesure que la taille de votre modèle augmente.
Warren P
De quelle manière? Cette approche continue de fonctionner sans changement car de nouvelles «choses» sont ajoutées à chaque objet. Les performances peuvent être un problème car la forme sérialisée des objets augmente en taille - mais cela n'a pas été un problème majeur. Le système est en développement continu depuis plus de 20 ans et est utilisé par des milliers d'utilisateurs.
Aardvark
1

J'ai dû faire cela lors de l'écriture d'un solveur pour un jeu de puzzle peg-jump. J'ai fait de chaque mouvement un objet Command qui contenait suffisamment d'informations pour que cela puisse être fait ou annulé. Dans mon cas, c'était aussi simple que de stocker la position de départ et la direction de chaque mouvement. J'ai ensuite stocké tous ces objets dans une pile afin que le programme puisse facilement annuler autant de mouvements que nécessaire tout en effectuant un retour en arrière.

Bill le lézard
la source
1

Vous pouvez essayer l'implémentation prête à l'emploi du modèle Undo / Redo dans PostSharp. https://www.postsharp.net/model/undo-redo

Il vous permet d'ajouter des fonctionnalités d'annulation / restauration à votre application sans implémenter le modèle vous-même. Il utilise un modèle enregistrable pour suivre les modifications de votre modèle et fonctionne avec le modèle INotifyPropertyChanged qui est également implémenté dans PostSharp.

Vous disposez de contrôles d'interface utilisateur et vous pouvez décider du nom et de la granularité de chaque opération.

Antonín Procházka
la source
0

J'ai travaillé une fois sur une application dans laquelle toutes les modifications apportées par une commande au modèle de l'application (c'est-à-dire CDocument ... nous utilisions MFC) étaient persistées à la fin de la commande en mettant à jour les champs dans une base de données interne maintenue dans le modèle. Nous n'avons donc pas eu à écrire de code d'annulation / de rétablissement distinct pour chaque action. La pile d'annulation se souvenait simplement des clés primaires, des noms de champ et des anciennes valeurs à chaque fois qu'un enregistrement était modifié (à la fin de chaque commande).

Agnel Kurian
la source
0

La première section de Design Patterns (GoF, 1994) présente un cas d'utilisation pour implémenter l'annulation / le rétablissement en tant que modèle de conception.

Peter Turner
la source
0

Vous pouvez rendre votre idée initiale performante.

Utilisez des structures de données persistantes et tenez-vous-en à conserver une liste de références à l'ancien état . (Mais cela ne fonctionne vraiment que si les opérations, toutes les données de votre classe d'état sont immuables, et toutes les opérations sur celle-ci retournent une nouvelle version --- mais la nouvelle version n'a pas besoin d'être une copie complète, remplacez simplement la copie des parties modifiées -on-écriture '.)

Matthias
la source
0

J'ai trouvé le modèle de commande très utile ici. Au lieu d'implémenter plusieurs commandes inversées, j'utilise la restauration avec une exécution retardée sur une deuxième instance de mon API.

Cette approche semble raisonnable si vous voulez un faible effort d'implémentation et une facilité de maintenance (et pouvez vous permettre la mémoire supplémentaire pour la 2ème instance).

Voir ici pour un exemple: https://github.com/thilo20/Undo/

Thilo
la source
-1

Je ne sais pas si cela va vous être utile, mais quand j'ai dû faire quelque chose de similaire sur l'un de mes projets, j'ai fini par télécharger UndoEngine à partir de http://www.undomadeeasy.com - un moteur merveilleux et je ne me souciais vraiment pas trop de ce qu'il y avait sous le capot - cela fonctionnait.

NativeBreed
la source
Veuillez publier vos commentaires comme réponse uniquement si vous êtes sûr de fournir des solutions! Sinon, préférez le poster en commentaire sous la question! (si cela ne permet pas de le faire maintenant! veuillez attendre que vous ayez une bonne réputation)
InfantPro'Aravind '11
-1

À mon avis, l'UNDO / REDO pourrait être mis en œuvre de deux manières au sens large. 1. Niveau de commande (appelé niveau de commande Undo / Redo) 2. Niveau de document (appelé global Undo / Redo)

Niveau de commande: comme le soulignent de nombreuses réponses, cela est efficacement réalisé en utilisant le modèle Memento. Si la commande prend également en charge la journalisation de l'action, une restauration est facilement prise en charge.

Limitation: une fois que la portée de la commande est sortie, l'annulation / le rétablissement est impossible, ce qui conduit à l'annulation / rétablissement au niveau du document (global)

Je suppose que votre cas s'intégrerait dans l'annulation / le rétablissement global car il convient à un modèle qui implique beaucoup d'espace mémoire. En outre, cela convient également pour annuler / rétablir sélectivement. Il existe deux types primitifs

  1. Annulation / rétablissement de toute la mémoire
  2. Niveau objet Annuler Refaire

Dans "Toute la mémoire Undo / Redo", toute la mémoire est traitée comme une donnée connectée (comme un arbre, une liste ou un graphique) et la mémoire est gérée par l'application plutôt que par l'OS. Ainsi, les opérateurs new et delete si en C ++ sont surchargés pour contenir des structures plus spécifiques pour implémenter efficacement des opérations telles que a. Si un nœud est modifié, b. conserver et effacer les données, etc., son fonctionnement consiste essentiellement à copier toute la mémoire (en supposant que l'allocation de mémoire est déjà optimisée et gérée par l'application à l'aide d'algorithmes avancés) et à la stocker dans une pile. Si la copie de la mémoire est demandée, l'arborescence est copiée en fonction de la nécessité d'avoir une copie superficielle ou profonde. Une copie complète est effectuée uniquement pour cette variable qui est modifiée. Étant donné que chaque variable est allouée à l'aide d'une allocation personnalisée, l'application a le dernier mot pour la supprimer le cas échéant. Les choses deviennent très intéressantes si nous devons partitionner le Undo / Redo quand il se trouve que nous devons annuler / rétablir de manière sélective par programme un ensemble d'opérations. Dans ce cas, seules ces nouvelles variables, ou les variables supprimées ou les variables modifiées reçoivent un drapeau afin que Annuler / Rétablir uniquement annule / répète ces mémoires. Lorsque tel est le cas, une nouvelle idée de «modèle de visiteur» est utilisée. Il s’appelle «Annuler / rétablir au niveau de l’objet» ou les variables supprimées ou les variables modifiées reçoivent un drapeau pour qu'Undo / Redo n'annule / ne rétablisse que ces mémoires. Les choses deviennent encore plus intéressantes si nous devons faire un Undo / Redo partiel à l'intérieur d'un objet. Lorsque tel est le cas, une nouvelle idée de «modèle de visiteur» est utilisée. Il s’appelle «Annuler / rétablir au niveau de l’objet» ou les variables supprimées ou les variables modifiées reçoivent un drapeau pour qu'Undo / Redo n'annule / ne rétablisse que ces mémoires. Les choses deviennent encore plus intéressantes si nous devons faire un Undo / Redo partiel à l'intérieur d'un objet. Lorsque tel est le cas, une nouvelle idée de «modèle de visiteur» est utilisée. Il s’appelle «Annuler / rétablir au niveau de l’objet»

  1. Niveau objet Undo / Redo: Lorsque la notification d'annulation / de rétablissement est appelée, chaque objet implémente une opération de streaming dans laquelle, le streamer obtient de l'objet les anciennes données / nouvelles données qui sont programmées. Les données qui ne sont pas perturbées ne sont pas perturbées. Chaque objet reçoit un streamer comme argument et à l'intérieur de l'appel UNDo / Redo, il diffuse / déprime les données de l'objet.

1 et 2 peuvent avoir des méthodes telles que 1. BeforeUndo () 2. AfterUndo () 3. BeforeRedo () 4. AfterRedo (). Ces méthodes doivent être publiées dans la commande de base Undo / Redo (pas dans la commande contextuelle) afin que tous les objets implémentent également ces méthodes pour obtenir une action spécifique.

Une bonne stratégie consiste à créer un hybride de 1 et 2. La beauté est que ces méthodes (1 et 2) utilisent elles-mêmes des modèles de commande

Parthasarathy SRINIVASAN
la source