design patterns - patrones - Patrón de diseño para deshacer el motor
patrones de diseño java (22)
Estoy escribiendo una herramienta de modelado estructural para una aplicación de ingeniería civil. Tengo una gran 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 en el modelo. Ahora comencé a pensar si podría haber codificado de manera diferente. En lugar de guardar las copias en profundidad, 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ías a cabo los comandos simples que cambian las propiedades de los objetos, 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?
Acabo de leer sobre el patrón de comando en mi ágil libro de desarrollo. ¿Tal vez eso tiene 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.
más información Command-Pattern
Como otros han declarado, 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 al patrón de comando.
Al implementar deshacer / rehacer usando el patrón de comando, puede evitar grandes cantidades de código duplicado al resumir (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 (aparte de la administración del portapapeles). En otras palabras, la operación de deshacer para un corte es pegar y se corta la operación de deshacer para pegar. Esto se aplica a operaciones mucho más simples como tipear y borrar 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 un objeto deshacer, modificar el documento" puede "crear un objeto deshacer, ejecutar la operación rehacer en el objeto deshacer para modificar el documento".
Ahora, hay que admitir que muchas personas piensan para sí mismas "Bueno, ¿no es parte del objetivo 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 habrá comandos que sean específicos de las operaciones inmediatas y deshacer / rehacer, pero la reducción de la duplicación hará que el código sea más fácil de mantener.
Como referencia, aquí hay una implementación simple del patrón Command para Deshacer / Rehacer en C #: sistema simple de deshacer / rehacer para C # .
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 PO. Trabajarían, pero sería mucho trabajo mantener y extender.
Para este tipo de problema, creo que debe incorporar compatibilidad con su modelo de datos para admitir puntos de control diferenciales para cada objeto involucrado en el modelo. Lo hice una vez y funcionó muy bien. Lo más importante que tiene que hacer es evitar el uso directo de punteros o referencias en el modelo.
Toda referencia a otro objeto usa algún identificador (como un entero). Cada vez que se necesita el objeto, busca la definición actual del objeto de una tabla. La tabla contiene una lista vinculada para cada objeto que contiene todas las versiones anteriores, junto con información sobre el punto de control para el que estuvieron activos.
La implementación de deshacer / rehacer es simple: realice su acción y establezca un nuevo punto de control; revertir todas las versiones de objeto al punto de control anterior.
Se necesita cierta disciplina en el código, pero tiene muchas ventajas: no necesita copias profundas ya que está haciendo un almacenamiento diferencial del estado del modelo; puede determinar la cantidad de memoria que desea usar ( muy importante para cosas como los modelos CAD) ya sea por el número de redos o la 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.
En mi opinión, el UNDO / REDO podría implementarse de 2 maneras generales. 1. Nivel de comando (llamado nivel de comando Deshacer / Rehacer) 2. Nivel del documento (llamado Deshacer / Rehacer global)
Nivel de comando: como señalan muchas respuestas, esto se logra eficientemente usando el patrón Memento. Si el comando también admite el registro de la acción, se puede respaldar fácilmente una repetición.
Limitación: una vez que el alcance del comando está fuera, el deshacer / rehacer es imposible, lo que lleva al nivel de documento (global) deshacer / rehacer
Supongo que su caso cabría en el deshacer / rehacer global, ya que es adecuado para un modelo que implica mucho espacio de memoria. Además, esto también es adecuado para deshacer / rehacer selectivamente. Hay dos tipos primitivos
- Toda la memoria deshacer / rehacer
- Nivel de objeto Deshacer Rehacer
En "All memory Undo / Redo", toda la memoria se trata como datos conectados (como un árbol, o una lista o un gráfico) y la memoria es administrada por la aplicación en lugar del sistema operativo. Por lo tanto, los operadores nuevos y eliminados si están sobrecargados en C ++ deben contener estructuras más específicas para implementar de manera efectiva operaciones como a. Si se modifica cualquier nodo, b. mantener 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 usando 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 utilizando la asignación personalizada, la aplicación tiene el último comentario sobre cuándo eliminarla si es necesario. Las cosas se vuelven muy interesantes si tenemos que particionar el Deshacer / Rehacer cuando sucede que necesitamos desmantelar / rehacer programado-selectivamente un conjunto de operaciones. En este caso, solo esas nuevas variables, o variables eliminadas o variables modificadas reciben una bandera para que Deshacer / Rehacer solo deshaga / rehaga esa memoria. Las cosas se vuelven aún más interesantes si tenemos que 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 "Nivel de objeto Deshacer / rehacer"
- Nivel de objeto Deshacer / Rehacer: cuando se llama a la notificación para deshacer / rehacer, cada objeto implementa una operación de transmisión en la que, el transmisor recibe del objeto los datos antiguos / datos nuevos que están programados. La información que no se altera se deja sin perturbar. Cada objeto recibe un streamer como argumento y dentro de la llamada UNDo / Redo, transmite / descomprime 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 Deshacer / Rehacer básico (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) usan patrones de comando
Es posible que desee consultar el código de Paint.NET para deshacer; tienen un buen sistema de deshacer. Probablemente sea un poco más simple de lo que necesitarás, pero podría darte algunas ideas y pautas.
-Adán
Este podría ser un caso donde CSLA es aplicable. Fue diseñado para proporcionar soporte de deshacer complejo a los objetos en las aplicaciones de Windows Forms.
Estoy con Mendelt Siebenga sobre el hecho de que deberías usar el patrón de comando. El patrón que usaste fue el Patrón Memento, que puede ser y será un gran desperdicio con el tiempo.
Dado que está trabajando en una aplicación que consume mucha memoria, debería poder especificar la cantidad de memoria que el motor de deshacer puede tomar, cuántos niveles de deshacer se guardan o algún almacenamiento en el que se mantendrán. Si no lo hace, pronto se enfrentarán errores debido a que la máquina no tiene memoria.
Le aconsejaría que verifique si existe un marco que ya creó 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 que ya está escrito, depurado y probado en escenarios reales. Sería útil si agregaste lo que estás escribiendo, para que la gente pueda recomendar marcos que conozcan.
He encontrado que el patrón Command es muy útil aquí. En lugar de implementar varios comandos inversos, estoy utilizando la reversión con la ejecución retrasada 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 extra para la segunda instancia).
Vea aquí para un ejemplo: https://github.com/thilo20/Undo/
Implementé sistemas complejos de deshacer exitosamente usando el patrón Memento, es muy fácil y tiene la ventaja de proporcionar también un marco Rehacer de forma natural. Un beneficio más sutil es que las acciones agregadas también pueden estar contenidas dentro de un solo Deshacer.
En pocas palabras, tienes dos montones 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 empuja hacia la derecha en la pila Rehacer.
Cómo se implementa el método para cambiar el estado de su documento depende completamente de su implementación. Si simplemente puede hacer 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 en profundidad, 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, usuario Shift: selecciona una carga de objetos para realizar una operación, como eliminar, cambiar el nombre, cambiar atributo), el código crea una nueva pila de deshacer como un único recuerdo y lo pasa a la operación real para agrega las operaciones individuales a. Por lo tanto, sus métodos de acción no necesitan (a) tener una pila global de qué 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 están en la memoria solamente, pero podrías persistir en deshacer la pila si lo deseas, supongo.
La mayoría de los ejemplos que he leído lo hacen usando el comando o el patrón memento. Pero puedes hacerlo sin patrones de diseño con una estructura de deque-structure simple.
La mayoría de los ejemplos que he visto usan una variante de Command-Pattern para esto. Cada acción del usuario que se puede deshacer obtiene su propia instancia de comando con toda la información para ejecutar la acción y deshacerla. A continuación, puede mantener una lista de todos los comandos que se han ejecutado y puede deshacerlos uno por uno.
La primera sección de Design Patterns (GoF, 1994) tiene un caso de uso para implementar el deshacer / rehacer como un patrón de diseño.
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 maravilloso motor y realmente no me importó demasiado lo que estaba debajo del capó, simplemente funcionó.
Puede hacer que su idea inicial funcione.
Use estructuras de datos persistentes y mantenga una lista de referencias al estado anterior . (Pero eso solo funciona si las operaciones de 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, simplemente reemplace la copia de las piezas modificadas -en-escribir ''.)
Puede probar la implementación ya hecha del patrón Deshacer / Rehacer en PostSharp. https://www.postsharp.net/model/undo-redo
Le permite agregar funcionalidad de deshacer / rehacer a su aplicación sin implementar el patrón usted mismo. Utiliza un patrón de grabación para seguir los cambios en su modelo y funciona con el patrón INotifyPropertyChanged que también se implementa en PostSharp.
Se le proporcionan controles de UI y puede decidir cuál será el nombre y la granularidad de cada operación.
Reutilizamos la carga del archivo y guardamos el código de serialización de "objetos" para obtener una forma conveniente de guardar y restaurar el estado completo de un objeto. Empujamos esos objetos serializados en la pila de deshacer, junto con cierta información sobre la operación que se realizó y sugiere deshacer esa operación si no hay suficiente información obtenida de los datos serializados. Deshacer y Rehacer es a menudo simplemente reemplazar un objeto por otro (en teoría).
Ha habido muchos errores debido a los punteros (C ++) a objetos que nunca fueron reparados a medida que realiza algunas secuencias de deshacer deshacer impares (aquellos lugares no actualizados para "identificar" los "identificadores" más seguros). Los errores en esta área a menudo ... ummm ... interesante.
Algunas operaciones pueden ser casos especiales de uso de velocidad / recursos, como dimensionar cosas, mover cosas.
La selección múltiple proporciona algunas complicaciones interesantes también. Afortunadamente ya teníamos un concepto de agrupación en el código. El comentario de Kristopher Johnson sobre los subtemas es bastante parecido a lo que hacemos.
Si hablamos de GoF, el patrón de Memento aborda específicamente deshacer.
Tuve que hacer esto al escribir un solucionador para un juego de rompecabezas de clavijas y saltos. Hice que cada movimiento fuera un objeto Command que contenía suficiente información como para que se pudiera realizar o deshacer. En mi caso, esto fue 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.
Una manera inteligente de manejar deshacer, que haría que su software también sea adecuado para la colaboración de múltiples usuarios, está implementando 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 operacional para objetos JSON en Javascript
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 conservaron al final del comando al actualizar los campos en una base de datos interna mantenida dentro del modelo. Así que 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).
Es un marco simple para agregar la funcionalidad Deshacer / Rehacer a sus aplicaciones, basado en el patrón de diseño de Comando clásico. Admite acciones de combinación, transacciones anidadas, ejecución diferida (ejecución en la transacción de nivel superior) y posible historial de deshacer no lineal (donde puede elegir entre varias acciones para rehacer).