Patrón de diseño para el Motor de Deshacer


Estoy escribiendo una herramienta de modelado estructural para una aplicación de ingeniería civil. Tengo una enorme clase de modelo que representa todo el edificio, que incluye colecciones de nodos, elementos de línea, cargas,etc. que también son clases personalizadas.

Ya he codificado un motor de deshacer que guarda una copia profunda después de cada modificación del modelo. Ahora empecé a pensar si podría haber codificado diferente. En lugar de guardar las copias profundas, tal vez podría guardar una lista de cada acción modificadora con un modificador inverso correspondiente. Para poder aplicar los modificadores inversos al modelo actual para deshacer, o los modificadores para rehacer.

Me puedo imaginar cómo llevarías a cabo comandos simples que cambian las propiedades del objeto, etc. Pero, ¿qué tal comandos complejos? Como insertar nuevos objetos de nodo en el modelo y agregar algunos objetos de línea que mantienen referencias a los nuevos nodos.

¿Cómo se implementaría eso?

Author: Ozgur Ozcitak, 2008-09-08

22 answers

La mayoría de los ejemplos que he visto usan una variante del Comando -Pattern para esto. Cada acción de usuario que se puede deshacer obtiene su propia instancia de comando con toda la información para ejecutar la acción y revertirla. A continuación, puede mantener una lista de todos los comandos que se han ejecutado y puede revertirlos uno por uno.

 84
Author: Mendelt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-03-29 00:07:52

Creo que tanto memento como command no son prácticos cuando se trata de un modelo del tamaño y el alcance que implica el OP. Funcionarían, pero sería mucho trabajo para mantener y extender.

Para este tipo de problema, creo que necesita crear soporte para su modelo de datos para soportar puntos de control diferenciales para cada objeto involucrado en el modelo. He hecho esto una vez y funcionó muy bien. Lo más importante que tienes que hacer es evitar el uso directo de punteros o referencias en el modelo.

Cada referencia a otro objeto usa algún identificador (como un entero). Siempre que se necesite el objeto, se busca la definición actual del objeto en una tabla. La tabla contiene una lista vinculada para cada objeto que contiene todas las versiones anteriores, junto con información sobre para qué punto de control estaban activos.

Implementar deshacer / rehacer es simple: Haga su acción y establezca un nuevo punto de control; revertir todas las versiones de objetos al punto de control anterior.

Requiere cierta disciplina en el código, pero tiene muchas ventajas: no necesita copias profundas ya que está haciendo almacenamiento diferencial del estado del modelo; puede medir la cantidad de memoria que desea usar ( muy importante para cosas como los modelos CAD) por el número de rehechos o memoria utilizada; muy escalable y de bajo mantenimiento para las funciones que operan en el modelo ya que no necesitan hacer nada para implementar deshacer/rehacer.

 29
Author: Jeff Kotula,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-02-17 21:09:48

Si estás hablando de GoF, el patrón Memento se dirige específicamente a deshacer.

 17
Author: Andy Whitfield,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 15:00:38

Como otros han dicho, el patrón de comandos es un método muy poderoso para implementar Deshacer/Rehacer. Pero hay una ventaja importante que me gustaría mencionar al patrón de comando.

Al implementar deshacer/rehacer usando el patrón de comandos, puede evitar grandes cantidades de código duplicado abstrayendo (hasta cierto punto) las operaciones realizadas en los datos y utilizar esas operaciones en el sistema deshacer/rehacer. Por ejemplo, en un editor de texto cortar y pegar son comandos complementarios (aparte de la gestión del portapapeles). En otras palabras, la operación deshacer para un corte es pegar y la operación deshacer para una pasta es cortar. Esto se aplica a operaciones mucho más simples como escribir y eliminar texto.

La clave aquí es que puede usar su sistema de deshacer/rehacer como el sistema de comandos principal para su editor. En lugar de escribir el sistema como "crear objeto deshacer, modificar el documento "puede" crear objeto deshacer, ejecutar la operación rehacer en el objeto deshacer para modificar el documento".

Ahora, es cierto que muchas personas están pensando para sí mismos "Bueno, duh, no es parte del punto del patrón de comando?"Sí, pero he visto demasiados sistemas de comandos que tienen dos conjuntos de comandos, uno para operaciones inmediatas y otro para deshacer/rehacer. No estoy diciendo que no habrá comandos que sean específicos para operaciones inmediatas y deshacer / rehacer, pero reducir la duplicación hará que el código sea más mantenible.

 15
Author: Torlack,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 17:12:38

Es posible que desee hacer referencia a la Paint.NET código para su deshacer - tienen un muy buen sistema de deshacer. Probablemente sea un poco más simple de lo que necesitarás, pero podría darte algunas ideas y pautas.

-Adam

 8
Author: Adam Davis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 14:05:50

Este podría ser un caso donde CSLA es aplicable. Fue diseñado para proporcionar soporte de deshacer complejo a objetos en aplicaciones de Windows Forms.

 7
Author: Eric Z Beard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 14:10:15

He implementado sistemas de deshacer complejos con éxito utilizando el patrón Memento - muy fácil, y tiene la ventaja de proporcionar naturalmente un marco de Rehacer también. Un beneficio más sutil es que las acciones agregadas también se pueden contener dentro de una sola Deshacer.

En pocas palabras, tienes dos pilas de objetos memento. Uno para Deshacer, el otro para Rehacer. Cada operación crea un nuevo recuerdo, que idealmente serán algunas llamadas para cambiar el estado de su modelo, documento (o lo que sea). Esto se añade a la pila de deshacer. Cuando realiza una operación de deshacer, además de ejecutar la acción de Deshacer en el objeto Memento para volver a cambiar el modelo, también saca el objeto de la pila de deshacer y lo empuja directamente a la pila de Rehacer.

Cómo se implementa el método para cambiar el estado de su documento depende completamente de su implementación. Si simplemente puedes hacer una llamada a la API (por ejemplo, ChangeColour (r, g, b)), entonces precede con una consulta para obtener y guardar el estado correspondiente. Pero el patrón también soportará hacer copias profundas, instantáneas de memoria, creación de archivos temporales, etc. - todo depende de usted, ya que es simplemente una implementación de método virtual.

Para realizar acciones agregadas (por ejemplo, cambio de usuario: selecciona una carga de objetos para realizar una operación, como eliminar, renombrar, cambiar atributo), su código crea una nueva pila de Deshacer como un solo recuerdo, y lo pasa a la operación real para agregar las operaciones individuales. Por lo tanto, sus métodos de acción no necesitan (a) tener una pila global para preocuparse acerca de y (b) pueden codificarse de la misma manera, ya sea que se ejecuten de forma aislada o como parte de una operación agregada.

Muchos sistemas de deshacer están solo en memoria, pero podría persistir la pila de deshacer si lo desea, supongo.

 6
Author: Greg Whitfield,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 17:03:29

Acabo de leer sobre el patrón de comandos en mi libro de desarrollo ágil-tal vez eso tiene potencial?

Puede hacer que cada comando implemente la interfaz de comandos (que tiene un método Execute ()). Si desea deshacer, puede agregar un método de deshacer.

Más información aquí

 5
Author: Dave Arkell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 14:02:08

Estoy con Mendelt Siebenga en el hecho de que debe usar el Patrón de comandos. El patrón que usaste fue el Patrón de Memento, que puede y se volverá muy derrochador con el tiempo.

Dado que está trabajando en una aplicación que consume mucha memoria, debería poder especificar cuánta memoria puede ocupar el motor de deshacer, cuántos niveles de deshacer se guardan o algún almacenamiento en el que se conservarán. Si no lo hace, pronto se enfrentará a los errores resultantes de la máquina sin memoria.

Le aconsejaría que compruebe si hay un framework que ya haya creado un modelo para undos en el lenguaje de programación / framework de su elección. Es bueno inventar cosas nuevas, pero es mejor tomar algo ya escrito, depurado y probado en escenarios reales. Ayudaría si agregaras en qué estás escribiendo esto, para que la gente pueda recomendar frameworks que conocen.

 4
Author: Omer van Kloeten,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:02:11

Proyecto Codeplex :

Es un marco simple para agregar funcionalidad de Deshacer/Rehacer a sus aplicaciones, basado en el patrón de diseño de comandos clásico. Admite acciones de fusión, transacciones anidadas, ejecución retardada (ejecución en la confirmación de transacción de nivel superior) y posible historial de deshacer no lineal (donde puede elegir entre varias acciones para rehacer).

 3
Author: sg7,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-13 00:11:01

La mayoría de los ejemplos que he leído lo hacen usando el comando o el patrón memento. Pero también puedes hacerlo sin patrones de diseño con una simple estructura deque .

 2
Author: Patrik Svensson,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 16:08:18

Una forma inteligente de manejar deshacer, que haría que su software también sea adecuado para la colaboración multiusuario, es implementar una transformación operativa de la estructura de datos.

Este concepto no es muy popular pero bien definido y útil. Si la definición le parece demasiado abstracta, este proyecto es un ejemplo exitoso de cómo se define e implementa una transformación operativa para objetos JSON en Javascript

 2
Author: danza,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-05 12:09:40

Como referencia, aquí hay una implementación simple del patrón de comandos para Deshacer/Rehacer en C#: Sistema simple de deshacer/rehacer para C#.

 2
Author: Tomas Andrle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-13 00:05:52

Hemos reutilizado el archivo cargar y guardar código de serialización para "objetos" para un formulario conveniente para guardar y restaurar todo el estado de un objeto. Colocamos esos objetos serializados en la pila de deshacer, junto con alguna información sobre qué operación se realizó y sugerencias sobre cómo deshacer esa operación si no hay suficiente información obtenida de los datos serializados. Deshacer y rehacer a menudo es simplemente reemplazar un objeto con otro (en teoría).

Ha habido MUCHOS MUCHOS errores debido a los punteros (C++) a objetos que nunca se arreglaron mientras realizaba algunas secuencias de deshacer rehacer extrañas (esos lugares no se actualizaron a "identificadores"de deshacer más seguros). Insectos en esta área a menudo ...ummm... Interesante....

Algunas operaciones pueden ser casos especiales para la velocidad/uso de recursos, como dimensionar cosas, mover cosas.

La selección múltiple también proporciona algunas complicaciones interesantes. Afortunadamente ya teníamos un concepto de agrupación en el código. Kristopher Johnson comentario sobre sub-items es bastante cerca de lo que hacemos.

 1
Author: Aardvark,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 16:43:12

Tuve que hacer esto al escribir un solucionador para un juego de rompecabezas de salto de clavija. Hice de cada movimiento un objeto de comando que contenía suficiente información para que se pudiera hacer o deshacer. En mi caso, esto era tan simple como almacenar la posición inicial y la dirección de cada movimiento. Luego almacené todos estos objetos en una pila para que el programa pudiera deshacer fácilmente tantos movimientos como fuera necesario mientras retrocedía.

 1
Author: Bill the Lizard,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-12-11 15:16:34

Puede probar la implementación ya hecha del patrón Deshacer/Rehacer en PostSharp. https://www.postsharp.net/model/undo-redo

Le permite agregar la funcionalidad deshacer/rehacer a su aplicación sin implementar el patrón usted mismo. Utiliza Patrón grabable para rastrear los cambios en su modelo y funciona con patrón INotifyPropertyChanged que también se implementa en PostSharp.

Se le proporcionan controles de interfaz de usuario y puede decidir cuál es el nombre y la granularidad de cada uno operación será.

 1
Author: Antonín Procházka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-04 10:55:55

Una vez trabajé en una aplicación en la que todos los cambios realizados por un comando en el modelo de la aplicación (es decir, CDocument... estábamos usando MFC) se persistieron al final del comando actualizando los campos en una base de datos interna mantenida dentro del modelo. Así que no tuvimos que escribir código de deshacer/rehacer por separado para cada acción. La pila de deshacer simplemente recordaba las claves principales, los nombres de campo y los valores antiguos cada vez que se cambiaba un registro (al final de cada comando).

 0
Author: Agnel Kurian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2008-09-08 15:17:19

La primera sección de Patrones de diseño (GoF, 1994) tiene un caso de uso para implementar el deshacer/rehacer como un patrón de diseño.

 0
Author: Peter Turner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2009-02-17 20:46:05

Puede hacer que su idea inicial sea performante.

Use estructuras de datos persistentes, y mantenga una lista de referencias al estado antiguo alrededor de. (Pero eso solo funciona realmente si las operaciones todos los datos en su clase de estado son inmutables, y todas las operaciones en él devuelven una nueva versión---pero la nueva versión no necesita ser una copia profunda, solo reemplace las partes cambiadas 'copy-on-write'.)

 0
Author: Matthias,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-07-26 06:36:06

He encontrado que el patrón de comandos es muy útil aquí. En lugar de implementar varios comandos inversos, estoy usando reversión con ejecución retardada en una segunda instancia de mi API.

Este enfoque parece razonable si desea un bajo esfuerzo de implementación y fácil mantenimiento (y puede permitirse la memoria adicional para la 2ª instancia).

Vea aquí un ejemplo: https://github.com/thilo20/Undo /

 0
Author: Thilo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-03-12 23:24:06

No se si esto va a ser de alguna utilidad para ti, pero cuando tuve que hacer algo similar en uno de mis proyectos, terminé descargando UndoEngine desde http://www.undomadeeasy.com - un motor maravilloso y realmente no me importaba demasiado lo que había debajo del capó - simplemente funcionó.

 -1
Author: NativeBreed,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-09-28 15:56:31

En mi opinión, el DESHACER/REHACER podría implementarse de 2 maneras en términos generales. 1. Nivel de comando (llamado nivel de comando Deshacer / Rehacer) 2. Nivel de documento (llamado global Undo/Redo)

Nivel de comando: Como muchas respuestas señalan, esto se logra de manera eficiente utilizando el patrón Memento. Si el comando también soporta journalizar la acción, un rehacer es fácilmente soportado.

Limitación: Una vez que el alcance del comando está fuera, el deshacer / rehacer es imposible, lo que conduce al nivel de documento (global) deshacer/rehacer

Supongo que su caso encajaría en el deshacer/rehacer global, ya que es adecuado para un modelo que implica una gran cantidad de espacio de memoria. Además, esto es adecuado para deshacer/rehacer selectivamente también. Hay dos tipos primitivos

  1. Toda la memoria deshacer / rehacer
  2. Nivel de objeto Deshacer Rehacer

En "All memory Undo/Redo", toda la memoria se trata como un dato conectado (como un árbol, una lista o un gráfico) y la memoria es administrada por la aplicación en lugar de por el OPERATIVO. Por lo tanto, los operadores new y delete si en C++ están sobrecargados para contener estructuras más específicas para implementar efectivamente operaciones como a. Si se modifica algún nodo, b. mantener y borrar datos, etc., La forma en que funciona es básicamente copiar toda la memoria(suponiendo que la asignación de memoria ya está optimizada y administrada por la aplicación utilizando algoritmos avanzados) y almacenarla en una pila. Si se solicita la copia de la memoria, la estructura de árbol se copia en función de la necesidad de copia superficial o profunda. Una copia profunda se hace solo para esa variable que se modifica. Dado que cada variable se asigna mediante asignación personalizada, la aplicación tiene la última palabra cuando eliminarla si es necesario. Las cosas se vuelven muy interesantes si tenemos que particionar el Deshacer/Rehacer cuando sucede que necesitamos Deshacer/Rehacer de forma programática y selectiva un conjunto de operaciones. En este caso, solo las variables nuevas, las variables eliminadas o las variables modificadas reciben un indicador para que solo se Deshagan/rehagan deshace / rehace esos recuerdos Las cosas se vuelven aún más interesantes si necesitamos hacer un Deshacer/Rehacer parcial dentro de un objeto. Cuando tal es el caso, se utiliza una idea más nueva de "Patrón de visitante". Se llama "Object Level Undo / redo"

  1. A nivel de objeto Deshacer/Rehacer: Cuando se llama la notificación para deshacer/rehacer, cada objeto implementa una operación de streaming en la que, el streamer obtiene del objeto los datos antiguos/nuevos datos que se programan. Los datos que no se alteran se dejan inalterados. Cada objeto recibe un streamer como argumento y dentro de la llamada Deshacer/Rehacer, transmite / des-transmite los datos del objeto.

Tanto 1 como 2 podrían tener métodos como 1. BeforeUndo() 2. AfterUndo() 3. AntesdEedo() 4. AfterRedo (). Estos métodos deben publicarse en el comando básico Deshacer / rehacer (no el comando contextual) para que todos los objetos implementen estos métodos también para obtener una acción específica.

Una buena estrategia es crear un híbrido de 1 y 2. La belleza es que estos los propios métodos(1&2) usan patrones de comando

 -1
Author: Parthasarathy SRINIVASAN,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-05-06 13:29:54