Patrón de diseño para motor de deshacer


117

Estoy escribiendo una herramienta de modelado estructural para una aplicación de ingeniería civil. Tengo una clase de modelo enorme 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 comencé a pensar si podría haber codificado de manera diferente. En lugar de guardar las copias en profundidad, quizás 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.

Puedo imaginar cómo llevaría a cabo comandos simples que cambian las propiedades del objeto, etc. Pero, ¿qué hay de los 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?


Si agrego el comentario "Deshacer Algorthim", ¿puedo buscar "Deshacer Algoritmo" y encontrar esto? Eso es lo que busqué y encontré algo cerrado como duplicado.
Peter Turner

Hay, también quiero desarrollar deshacer / rehacer en la aplicación que estamos desarrollando. Usamos el marco QT4 y necesitamos tener muchas acciones complejas de deshacer / rehacer ... Me preguntaba, ¿ha tenido éxito usando Command-Pattern?
Ashika Umanga Umagiliya

2
@umanga: Funcionó pero no fue fácil. La parte más difícil fue hacer un seguimiento de las referencias. Por ejemplo, cuando se elimina un objeto Frame, sus objetos secundarios: nodos, cargas que actúan sobre él y muchas otras asignaciones de usuario deben conservarse para reinsertarse cuando se deshagan. Pero algunos de estos objetos secundarios se compartieron con otros objetos, y la lógica de deshacer / rehacer se volvió bastante compleja. Si el modelo no fuera tan grande, mantendría el enfoque de recuerdo; es mucho más fácil de implementar.
Ozgur Ozcitak

Este es un problema divertido para trabajar, piense en cómo lo hacen los repositorios de código fuente, como svn (mantienen las diferencias entre confirmaciones).
Alex

Respuestas:


88

La mayoría de los ejemplos que he visto usan una variante del Command-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. Luego puede mantener una lista de todos los comandos que se han ejecutado y puede revertirlos uno por uno.


4
Básicamente, así es como funciona el motor de deshacer en Cocoa, NSUndoManager.
amrox

33

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

Para este tipo de problema, creo que debe incorporar el soporte a su modelo de datos para admitir puntos de control diferenciales para cada objeto involucrado en el modelo. Hice esto una vez y funcionó muy bien. Lo más importante que debe hacer es evitar el uso directo de punteros o referencias en el modelo.

Cada referencia a otro objeto usa algún identificador (como un número entero). Siempre que se necesita el objeto, 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: realice su acción y establezca un nuevo punto de control; Revertir todas las versiones del objeto al punto de control anterior.

Requiere algo de 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 modelos CAD) por número de rehacer o memoria usada; muy escalable y de bajo mantenimiento para las funciones que operan en el modelo, ya que no necesitan hacer nada para implementar deshacer / rehacer.


1
Si utiliza una base de datos (por ejemplo, sqlite) como formato de archivo, esto puede ser casi automático
Martin Beckett

4
Si aumenta esto mediante el seguimiento de las dependencias introducidas por los cambios en el modelo, entonces podría tener un sistema de árbol de deshacer (es decir, si cambio el ancho de una viga, luego voy a trabajar en un componente separado, puedo regresar y deshacer la viga cambia sin perder el resto). La interfaz de usuario para eso puede ser un poco difícil de manejar, pero sería mucho más poderosa que un deshacer lineal tradicional.
Sumudu Fernando

¿Puedes explicar más esta idea de id vs punteros? ¿Seguramente un puntero / dirección de memoria funciona tan bien como id?
paulm

@paulm: esencialmente, los datos reales están indexados por (id, versión). Los punteros se refieren a una versión particular de un objeto, pero usted busca hacer referencia al estado actual de un objeto, cualquiera que sea, por lo que desea abordarlo por id, no por (id, versión). Usted podría reestructurarlo para que almacenar un puntero a la (=> datos de la versión) mesa y sólo debes elegir la última cada vez, pero que tiende a localidad daño cuando estás persistente de datos, enturbia se refiere a un poco, y hace que sea más difícil hacer algunos tipos de consultas comunes, por lo que no es la forma en que se haría normalmente.
Chris Morgan

17

Si está hablando de GoF, el patrón Memento trata específicamente de deshacer.


7
Realmente no, esto aborda su enfoque inicial. Está pidiendo un enfoque alternativo. El inicial almacena el estado completo para cada paso mientras que el último almacena solo las "diferencias".
Andrei Rînea

15

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

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

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

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


1
Nunca pensé pasteen cut^ -1.
Lenar Hoyt

8

Es posible que desee consultar el código Paint.NET para su deshacer: tienen un sistema de deshacer realmente bueno. Probablemente sea un poco más simple de lo que necesitará, pero podría brindarle algunas ideas y pautas.

-Adán


4
En realidad, el código Paint.NET ya no está disponible, pero puede obtener el código
Igor Brejc

7

Este podría ser un caso en el que se aplique CSLA . Fue diseñado para proporcionar un soporte complejo para deshacer objetos en aplicaciones de Windows Forms.


6

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

En pocas palabras, tienes dos pilas de objetos de recuerdo. 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 agrega a la pila de deshacer. Cuando realiza una operación de deshacer, además de ejecutar la acción Deshacer en el objeto Memento para volver a cambiar el modelo, también saca el objeto de la pila Deshacer y lo coloca directamente en la pila Rehacer.

La forma en que se implementa el método para cambiar el estado de su documento depende completamente de su implementación. Si simplemente puede realizar una llamada a la API (por ejemplo, ChangeColour (r, g, b)), preceda con una consulta para obtener y guardar el estado correspondiente. Pero el patrón también admitirá la realización de copias profundas, instantáneas de memoria, creación de archivos temporales, etc. Depende de usted, ya que es simplemente una implementación de método virtual.

Para realizar acciones agregadas (por ejemplo, el usuario Shift-Selecciona una carga de objetos para realizar una operación, como eliminar, renombrar, cambiar atributo), su código crea una nueva pila Deshacer como un solo recuerdo y lo pasa a la operación real a agregue las operaciones individuales a. Por lo tanto, sus métodos de acción no necesitan (a) tener una pila global de la que preocuparse 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 solo están en memoria, pero supongo que podría conservar la pila de deshacer si lo desea.


5

Acabo de leer sobre el patrón de comando en mi libro de desarrollo ágil, ¿tal vez eso tenga potencial?

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

mas info aqui


4

Estoy con Mendelt Siebenga sobre el hecho de que debería usar el patrón de comando. El patrón que usó fue el patrón Memento, que puede y será un desperdicio 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 algo de almacenamiento en el que se conservarán. Si no hace esto, pronto enfrentará errores resultantes de que la máquina no tenga memoria.

Le aconsejo que compruebe si hay un marco que ya haya creado un modelo para deshacer en el lenguaje / marco de programación de su elección. Es bueno inventar cosas nuevas, pero es mejor tomar algo ya escrito, depurado y probado en escenarios reales. Sería útil si agregara lo que está escribiendo esto, para que las personas puedan recomendar marcos que conocen.


3

Proyecto Codeplex :

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


2

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


¿Qué pondrías en el deque?

En mi caso, puse el estado actual de las operaciones para las que quería deshacer / rehacer la funcionalidad. Al tener dos deques (deshacer / rehacer), deshago en la cola de deshacer (muestra el primer elemento) y lo inserto en la cola de rehacer. Si el número de elementos en las colas excede el tamaño preferido, hago estallar un elemento de la cola.
Patrik Svensson

2
Lo que describe en realidad ES un patrón de diseño :). El problema con este enfoque es cuando su estado requiere mucha memoria: mantener varias docenas de versiones de estado se vuelve poco práctico o incluso imposible.
Igor Brejc

O puede almacenar un par de cierres que representen la operación normal y deshacer.
Xwtek

2

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

Este concepto no es muy popular pero está bien definido y es ú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.



1

Reutilizamos la carga de archivos y guardamos el código de serialización para "objetos" para una forma conveniente de guardar y restaurar el estado completo de un objeto. Colocamos esos objetos serializados en la pila de deshacer, junto con cierta información sobre qué operación se realizó y sugerencias para deshacer esa operación si no hay suficiente información obtenida de los datos serializados. Deshacer y rehacer a menudo es solo reemplazar un objeto por otro (en teoría).

Ha habido muchos MUCHOS errores debido a punteros (C ++) a objetos que nunca fueron arreglados mientras realiza algunas secuencias de deshacer rehacer extrañas (esos lugares no se actualizan para "identificadores" más seguros para deshacer). Errores en esta área a menudo ... ummm ... interesante.

Algunas operaciones pueden ser casos especiales de 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. El comentario de Kristopher Johnson sobre los sub-elementos está bastante cerca de lo que hacemos.


Esto suena cada vez más impracticable a medida que aumenta el tamaño de su modelo.
Warren P

¿En qué manera? Este enfoque sigue funcionando sin cambios a medida que se agregan nuevas "cosas" a cada objeto. El rendimiento podría ser un problema a medida que la forma serializada de los objetos aumenta de tamaño, pero esto no ha sido un problema importante. El sistema ha estado en continuo desarrollo durante más de 20 años y está en uso por miles de usuarios.
Aardvark

1

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


1

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

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

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


0

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 conservaban al final del comando actualizando campos en una base de datos interna mantenida dentro del modelo. Por lo tanto, no tuvimos que escribir un código de deshacer / rehacer por separado para cada acción. La pila de deshacer simplemente recordaba las claves primarias, los nombres de los campos y los valores antiguos cada vez que se cambiaba un registro (al final de cada comando).


0

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


0

Puede hacer que su idea inicial sea eficaz.

Utilice estructuras de datos persistentes y mantenga una lista de referencias al estado anterior . (Pero eso solo funciona realmente si las operaciones, todos los datos en su clase de estado son inmutables, y todas las operaciones devuelven una nueva versión, pero la nueva versión no necesita ser una copia profunda, simplemente reemplace la copia de las partes cambiadas -sin escribir '.)


0

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

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

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


-1

No sé si esto te va a ser útil, pero cuando tuve que hacer algo similar en uno de mis proyectos, terminé descargando UndoEngine de http://www.undomadeeasy.com , un motor maravilloso. y realmente no me importaba demasiado lo que había debajo del capó, simplemente funcionaba.


¡Publique sus comentarios como respuesta solo si está seguro de brindar soluciones! De lo contrario, prefiera publicarlo como comentario debajo de la pregunta. (si no lo permite ahora, espere hasta que obtenga una buena reputación)
InfantPro'Aravind '

-1

En mi opinión, UNDO / REDO podría implementarse de 2 maneras en general. 1. Nivel de comando (llamado nivel de comando Deshacer / Rehacer) 2. Nivel de documento (llamado Deshacer / Rehacer global)

Nivel de comando: como señalan muchas respuestas, esto se logra de manera eficiente utilizando el patrón Memento. Si el comando también admite registrar la acción en un diario, se admite fácilmente una rehacer.

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

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

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

En "Deshacer / Rehacer toda la memoria", toda la memoria se trata como un dato conectado (como un árbol, una lista o un gráfico) y la memoria la administra la aplicación en lugar del sistema operativo. Por lo tanto, los operadores nuevos y de eliminación si están en C ++ están sobrecargados para contener estructuras más específicas para implementar de manera efectiva operaciones como a. Si se modifica algún nodo, b. guardar y borrar datos, etc., la forma en que funciona es básicamente copiar toda la memoria (asumiendo que la asignación de memoria ya está optimizada y administrada por la aplicación mediante algoritmos avanzados) y almacenarla en una pila. Si se solicita la copia de la memoria, la estructura del árbol se copia en función de la necesidad de tener una copia superficial o profunda. Se realiza una copia profunda solo para esa variable que se modifica. Dado que cada variable se asigna mediante una asignación personalizada, la aplicación tiene la última palabra cuando eliminarla si es necesario. Las cosas se vuelven muy interesantes si tenemos que dividir Deshacer / Rehacer cuando sucede que necesitamos Deshacer / Rehacer programáticamente-selectivamente un conjunto de operaciones. En este caso, solo esas nuevas variables, o las variables eliminadas o las variables modificadas reciben una bandera para que Deshacer / Rehacer solo deshaga / rehace esa memoria. 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 "Deshacer / rehacer a nivel de objeto". o las variables eliminadas o modificadas reciben una bandera para que Deshacer / Rehacer solo deshaga / rehace esa memoria. 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 "Deshacer / rehacer a nivel de objeto". o las variables eliminadas o modificadas reciben una bandera para que Deshacer / Rehacer solo deshaga / rehace esa memoria. 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 "Deshacer / rehacer a nivel de objeto".

  1. Deshacer / Rehacer a nivel de objeto: cuando se llama a la notificación de deshacer / rehacer, cada objeto implementa una operación de transmisión en la que el transmisor obtiene del objeto los datos antiguos / nuevos que está programado. Los datos que no se alteran se dejan intactos. Cada objeto recibe un streamer como argumento y dentro de la llamada UNDo / Redo, transmite / desentraña los datos del objeto.

Tanto 1 como 2 podrían tener métodos como 1. BeforeUndo () 2. AfterUndo () 3. BeforeRedo () 4. AfterRedo (). Estos métodos deben publicarse en el comando básico Deshacer / rehacer (no en 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. Lo bueno es que estos métodos (1 y 2) utilizan patrones de comando

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.