Question

J'écris un outil de modélisation structurelle pour une application de génie civil.J'ai une énorme classe de modèles 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 complète 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 sauvegarder 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 modifieraient les propriétés d'un objet, etc.Mais qu’en est-il des commandes complexes ?C'est comme insérer de nouveaux objets nœuds dans le modèle et ajouter des objets ligne qui conservent les références aux nouveaux nœuds.

Comment procéder pour mettre cela en œuvre ?

Était-ce utile?

La solution

La plupart des exemples que j'ai vus utilisent une variante du Modèle de commande pour ça.Chaque action utilisateur annulable obtient sa propre instance de commande avec toutes les informations nécessaires pour exécuter l'action et l'annuler.Vous pouvez ensuite conserver une liste de toutes les commandes qui ont été exécutées et les annuler une par une.

Autres conseils

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 qu'implique le PO.Ils fonctionneraient, mais cela demanderait beaucoup de travail de les maintenir et de les é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és 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 identifiant (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.

L'implémentation d'annulation/rétablissement 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) par le nombre de restaurations ou la mémoire utilisée ;très évolutif et nécessitant peu de maintenance pour les fonctions qui opèrent sur le modèle puisqu'elles n'ont rien à faire pour implémenter annuler/rétablir.

Si vous parlez du GoF, le Mémento Le modèle traite spécifiquement de l'annulation.

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

Lors de l'implémentation d'annulation/rétablissement à 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/rétablissement.Par exemple dans un éditeur de texte couper et coller sont des commandes complémentaires (en dehors de la gestion du presse-papier).En d’autres termes, l’opération d’annulation d’une coupe est collée et l’opération d’annulation d’un collage est coupée.Cela s'applique à des opérations beaucoup plus simples telles que 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 un objet d'annulation pour modifier le document".

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

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 quelques idées et lignes directrices.

-Adam

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

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

En un mot, vous disposez de deux piles d’objets souvenirs.L'un pour annuler, l'autre pour rétablir.Chaque opération crée un nouveau souvenir, qui sera idéalement constitué de quelques appels pour changer l'état de votre modèle, document (ou autre).Ceci 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 retirez également l'objet de la pile Annuler et le poussez directement sur la pile Rétablir.

La manière dont la méthode pour modifier l’état de votre document est implémentée dépend entièrement de votre implémentation.Si vous pouvez simplement effectuer un appel API (par ex.ChangeColour(r,g,b)), puis 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, la 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 globales (par ex.utilisateur Shift-Sélectionne une charge d'objets sur lesquels effectuer une opération, telle que supprimer, renommer, modifier un attribut), votre code crée une nouvelle pile d'annulation en tant que souvenir unique et la transmet à l'opération réelle pour y ajouter les opérations individuelles.Ainsi, vos méthodes d'action n'ont pas besoin de (a) avoir une pile globale à gérer et (b) peuvent être codées de la même manière, qu'elles soient exécutées de manière isolée ou dans le cadre d'une opération globale.

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.

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

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

Plus d'informations ici

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

Puisque vous travaillez sur une application gourmande en mémoire, vous devriez pouvoir spécifier la quantité de mémoire que le moteur d'annulation est autorisé à occuper, le nombre de niveaux d'annulation enregistrés ou le stockage dans lequel ils seront conservés.Si vous ne le faites pas, vous serez bientôt confronté à des erreurs résultant d’un 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 d'annulation dans le langage/framework de programmation de votre choix.C'est bien d'inventer du nouveau, mais il vaut mieux prendre quelque chose de déjà écrit, débogué et testé dans des scénarios réels.Il serait utile que vous ajoutiez ce dans quoi vous écrivez ceci, afin que les gens puissent recommander les frameworks qu'ils connaissent.

Projet Codeplex:

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

La plupart des exemples que j'ai lus le font en utilisant soit le modèle command, soit le modèle memento.Mais vous pouvez également le faire sans modèles de conception avec un simple deque-structure.

Une façon intelligente de gérer l'annulation, qui rendrait votre logiciel également adapté à la collaboration multi-utilisateurs, consiste à implémenter un 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

Pour référence, voici une implémentation simple du modèle de commande pour Annuler/Rétablir en C# : Système simple d'annulation/rétablissement pour C#.

Nous avons réutilisé le chargement du fichier et enregistré le code de sérialisation pour les « objets » pour obtenir un formulaire pratique permettant de sauvegarder et de restaurer l'intégralité de l'état d'un objet.Nous plaçons 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 pour annuler cette opération s'il n'y a pas suffisamment d'informations glanées à partir des données sérialisées.Annuler et refaire consiste souvent simplement à remplacer un objet par un autre (en théorie).

Il y a eu de nombreux BEAUCOUP de bugs dus à des pointeurs (C++) vers des objets qui n'ont jamais été corrigés lorsque vous effectuez d'étranges séquences d'annulation et de rétablissement (ces endroits non mis à jour pour des « identifiants » d'annulation plus sûrs).Des bugs dans ce domaine sont souvent... euh...intéressant.

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

La sélection multiple offre é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.

J'ai dû le faire lors de l'écriture d'un solveur pour un jeu de puzzle à saut de cheville.J'ai fait de chaque mouvement un objet Commande contenant suffisamment d'informations pour qu'il puisse être effectué 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 lors du retour en arrière.

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

Il vous permet d'ajouter une fonctionnalité d'annulation/rétablissement à votre application sans implémenter le modèle vous-même.Il utilise le modèle Recordable pour suivre les modifications apportées à 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.

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

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

Vous pouvez rendre votre idée initiale performante.

Utiliser structures de données persistantes, et tenez-vous-en à garder un liste des références à l'ancien état autour.(Mais cela ne fonctionne vraiment que si les opérations sur toutes les données de votre classe d'état sont immuables et que toutes les opérations sur celles-ci renvoient 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 -sur-écriture'.)

J'ai trouvé le modèle Command très utile ici.Au lieu d'implémenter plusieurs commandes inverses, j'utilise le rollback avec exécution différée sur une deuxième instance de mon API.

Cette approche semble raisonnable si vous souhaitez un faible effort de mise en œuvre et une maintenabilité facile (et pouvez vous permettre la mémoire supplémentaire pour la 2ème instance).

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

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

À mon avis, le UNDO/REDO pourrait être mis en œuvre de deux manières globales.1.Niveau de commande (appelé le niveau de commande undo / redo) 2.Niveau du document (appelé Annuler/Rétablir global)

Niveau de commande :Comme le soulignent de nombreuses réponses, ceci est réalisé efficacement 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 la portée de la commande dépassée, l'annulation/rétablissement est impossible, ce qui conduit à une annulation/rétablissement au niveau du document (global).

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

  1. Toute la mémoire annuler/rétablir
  2. Au niveau de l'objet Annuler Rétablir

Dans "Toute la mémoire Undo/Redo", la totalité de la mémoire est traitée comme une donnée connectée (comme une arborescence, une liste ou un graphique) et la mémoire est gérée par l'application plutôt que par le système d'exploitation.Ainsi, les opérateurs new et delete s'ils sont en C++ sont surchargés pour contenir des structures plus spécifiques afin d'implémenter efficacement des opérations telles que a.Si un nœud est modifié, b.En maintenant et en compensant les données, etc., la façon dont elle fonctionne est essentiellement de copier la mémoire entière (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 le stocker dans une pile.Si la copie de la mémoire est demandée, la structure arborescente 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 la variable modifiée.Étant donné que chaque variable est allouée à l’aide d’une allocation personnalisée, l’application a le dernier mot quant au moment de la supprimer si nécessaire.Les choses deviennent très intéressantes si nous devons partitionner l'opération Annuler/Rétablir lorsqu'il s'avère que nous devons annuler/rétablir de manière sélective par programme un ensemble d'opérations.Dans ce cas, seules les nouvelles variables, ou les variables supprimées ou les variables modifiées reçoivent un drapeau afin que l'annulation / refaire n'infondait / ne redde que ces choses de mémoire deviennent encore plus intéressantes si nous avons besoin de faire une analyse / refaire partielle à l'intérieur d'un objet.Lorsque tel est le cas, une idée plus récente de « modèle de visiteur » est utilisée.C'est ce qu'on appelle "Annuler/rétablir au niveau de l'objet".

  1. Annuler/Rétablir au niveau de l'objet :Lorsque la notification d'annulation/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 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 dans l'appel UNDo/Redo, il diffuse/désactive les données de l'objet.

1 et 2 pourraient avoir des méthodes telles que 1.AVANTUDO () 2.Afterundo () 3.BeForredo () 4.AprèsRedo().Ces méthodes doivent être publiées dans la commande Undo/redo de base (et non 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&2) elles-mêmes utilisent des modèles de commandes

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top