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?
fuente
Respuestas:
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.
fuente
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.
fuente
Si está hablando de GoF, el patrón Memento trata específicamente de deshacer.
fuente
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.
fuente
paste
encut
^ -1.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
fuente
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.
fuente
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.
fuente
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
fuente
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.
fuente
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).
fuente
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 .
fuente
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.
fuente
Como referencia, aquí hay una implementación simple del patrón de comando para Deshacer / Rehacer en C #: Sistema simple de deshacer / rehacer para C # .
fuente
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.
fuente
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.
fuente
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.
fuente
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).
fuente
La primera sección de Design Patterns (GoF, 1994) tiene un caso de uso para implementar deshacer / rehacer como patrón de diseño.
fuente
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 '.)
fuente
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/
fuente
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.
fuente
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
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".
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
fuente