2009-12-16 14 views

Respuesta

27

Hay dos patrones clásicos para usar. El primero es memento pattern que se utiliza para almacenar instantáneas de su estado de objeto completo. Esto es quizás más intensivo en el sistema que el patrón de comando, pero permite retroceder de forma muy simple a una instantánea anterior. Puede almacenar las instantáneas en el disco a la PaintShop/PhotoShop o mantenerlas en la memoria para objetos más pequeños que no requieren persistencia. Lo que estás haciendo es exactamente para lo que se diseñó este patrón, por lo que debería ajustarse a la factura un poco mejor que el patrón de comando sugerido por otros.

Además, una nota adicional es que, como no requiere tener comandos recíprocos para deshacer algo que se hizo previamente, significa que cualquier función potencialmente unidireccional [como hash o cifrado] que no se puede deshacer trivialmente utilizando comandos recíprocos aún se puede deshacer de manera muy simple simplemente volviendo a una instantánea anterior.

También como se ha señalado, la command pattern que es potencialmente menos recursos, así que admitir que, en casos específicos en los que:

  • hay un estado objeto grande que se persistió y/o
  • Hay hay métodos destructivos y
  • Dónde comandos recíprocos pueden usarse muy trivial para revertir cualquier acción tomada

el patrón de comandos puede estar en mejor forma [pero no necesariamente, dependerá mucho de la situación]. En otros casos, usaría el patrón de recuerdo.

Probablemente me abstendría de usar una mezcla de los dos porque me suele importar el desarrollador que va a venir detrás de mí y mantener mi código, así como también es mi responsabilidad ética con mi empleador hacer ese proceso como simple y económico como sea posible. Veo un mashup de los dos patrones que se convierte fácilmente en un agujero de molestia inmanejable que sería costoso de mantener.

+1

+1 Si el estado de sus objetos no es muy grande, esto es mucho más fácil de implementar que el patrón de comando deshacer/rehacer que puede hacerse complejo muy fácilmente . Y muchas veces tendrá comandos destructivos que no se pueden deshacer sin una copia de estado completo de todos modos. Pero para estados muy grandes, esto puede ser poco práctico. – Josh

+0

Eche un vistazo a las estructuras de datos puramente funcionales; pueden darte un intercambio considerable entre los datos en vivo actuales y los recuerdos. –

+0

Eche un vistazo a la diferencia de estado descrita en mi respuesta. Puede ser la mejor alternativa según tu caso de uso. – vincent

4

La práctica clásica es seguir el Command Pattern.

Puede encapsular cualquier objeto que realice una acción con un comando, y hacer que realice la acción inversa con un método Undo(). Almacena todas las acciones en una pila para una forma fácil de rebobinarlas.

+1

Esto no permite funciones de ida como de hash o funciones matemáticas que no se puede 'deshacerse' usando un método recíproco. Por lo tanto, este patrón debe usarse con cuidado para este enfoque. – BenAlabaster

+1

Eso es bastante cierto. Sin embargo, para cosas generales de UI, va un largo camino. – womp

+0

Pregunta aleatoria de alguien que nunca implementó uno de esos patrones: ¿No podría el patrón de comando almacenar fácilmente una instantánea del estado anterior que modificó para facilitar la operación de deshacer? ¿Algo así como una alianza impía entre Command y Memento? Especialmente, si se pueden deshacer trivialmente muchos comandos, almacenar instantáneas por cada acción puede ser un poco costoso. – Joey

2

Eche un vistazo a Command Pattern. Tiene que encapsular cada cambio en su modelo en objetos de comando separados.

+0

Ver mi comentario sobre la respuesta de Womp para usar el patrón de comando para mecanismos 'deshacer' – BenAlabaster

0

Escribí un sistema realmente flexible para realizar un seguimiento de los cambios.Tengo un programa de dibujo que implementa 2 tipos de cambios:

  • añadir/eliminar una forma
  • cambio de propiedad de una forma

Clase base:

public abstract class Actie 
{ 
    public Actie(Vorm[] Vormen) 
    { 
     vormen = Vormen; 
    } 

    private Vorm[] vormen = new Vorm[] { }; 
    public Vorm[] Vormen 
    { 
     get { return vormen; } 
    } 

    public abstract void Undo(); 
    public abstract void Redo(); 
} 

clase derivada de agregando formas:

public class VormenToegevoegdActie : Actie 
{ 
    public VormenToegevoegdActie(Vorm[] Vormen, Tekening tek) 
     : base(Vormen) 
    { 
     this.tek = tek; 
    } 

    private Tekening tek; 
    public override void Redo() 
    { 
     tek.Vormen.CanRaiseEvents = false; 
     tek.Vormen.AddRange(Vormen); 
     tek.Vormen.CanRaiseEvents = true; 
    } 

    public override void Undo() 
    { 
     tek.Vormen.CanRaiseEvents = false; 
     foreach(Vorm v in Vormen) 
      tek.Vormen.Remove(v); 
     tek.Vormen.CanRaiseEvents = true; 
    } 
} 

clase derivada de la eliminación de formas:

public class VormenVerwijderdActie : Actie 
{ 
    public VormenVerwijderdActie(Vorm[] Vormen, Tekening tek) 
     : base(Vormen) 
    { 
     this.tek = tek; 
    } 

    private Tekening tek; 
    public override void Redo() 
    { 
     tek.Vormen.CanRaiseEvents = false; 
     foreach(Vorm v in Vormen) 
      tek.Vormen.Remove(v); 
     tek.Vormen.CanRaiseEvents = true; 
    } 

    public override void Undo() 
    { 
     tek.Vormen.CanRaiseEvents = false; 
     foreach(Vorm v in Vormen) 
      tek.Vormen.Add(v); 
     tek.Vormen.CanRaiseEvents = true; 
    } 
} 

clase derivada de los cambios de propiedad:

public class PropertyChangedActie : Actie 
{ 
    public PropertyChangedActie(Vorm[] Vormen, string PropertyName, object OldValue, object NewValue) 
     : base(Vormen) 
    { 
     propertyName = PropertyName; 
     oldValue = OldValue; 
     newValue = NewValue; 
    } 

    private object oldValue; 
    public object OldValue 
    { 
     get { return oldValue; } 
    } 

    private object newValue; 
    public object NewValue 
    { 
     get { return newValue; } 
    } 

    private string propertyName; 
    public string PropertyName 
    { 
     get { return propertyName; } 
    } 

    public override void Undo() 
    { 
     //Type t = base.Vorm.GetType(); 
     PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName); 
     foreach(Vorm v in Vormen) 
     { 
      v.CanRaiseVeranderdEvent = false; 
      info.SetValue(v, oldValue, null); 
      v.CanRaiseVeranderdEvent = true; 
     } 
    } 
    public override void Redo() 
    { 
     //Type t = base.Vorm.GetType(); 
     PropertyInfo info = Vormen.First().GetType().GetProperty(propertyName); 
     foreach(Vorm v in Vormen) 
     { 
      v.CanRaiseVeranderdEvent = false; 
      info.SetValue(v, newValue, null); 
      v.CanRaiseVeranderdEvent = true; 
     } 
    } 
} 

Con cada vez Vormen = la matriz de elementos que se someten al cambio. Y debe ser utilizado de esta manera:

Declaración de las pilas:

Stack<Actie> UndoStack = new Stack<Actie>(); 
Stack<Actie> RedoStack = new Stack<Actie>(); 

Adición de una nueva forma (por ejemplo Point.)

VormenToegevoegdActie vta = new VormenToegevoegdActie(new Vorm[] { NieuweVorm }, this); 
UndoStack.Push(vta); 
RedoStack.Clear(); 

Extracción de una forma seleccionada

VormenVerwijderdActie vva = new VormenVerwijderdActie(to_remove, this); 
UndoStack.Push(vva); 
RedoStack.Clear(); 

Registrar un cambio de propiedad

PropertyChangedActie ppa = new PropertyChangedActie(new Vorm[] { (Vorm)e.Object }, e.PropName, e.OldValue, e.NewValue); 
UndoStack.Push(ppa); 
RedoStack.Clear(); 

Finalmente la acción de deshacer/rehacer

public void Undo() 
{ 
    Actie a = UndoStack.Pop(); 
    RedoStack.Push(a); 
    a.Undo(); 
} 

public void Redo() 
{ 
    Actie a = RedoStack.Pop(); 
    UndoStack.Push(a); 
    a.Redo(); 
} 

Creo que esta es la manera más efectiva de implementar un algoritmo de deshacer-rehacer. Para ver un ejemplo, mira esta página en mi sitio web: DrawIt.

Implementé las cosas deshacer rehacer alrededor de la línea 479 del archivo Tekening.cs. Puede descargar el código fuente. Puede ser implementado por cualquier tipo de aplicación.

+2

Tenga en cuenta que si desea promocionar su propio producto/blog, ** debe divulgar su afiliación en la respuesta **, de lo contrario, su respuesta puede marcarse como spam. Si no está afiliado al sitio, le recomiendo que lo diga para evitarlo. Por favor, lea [Cómo no ser un spammer] (https://stackoverflow.com/help/promotion) – Mithrandir

0

Aquí hay tres enfoques que son viables. Patrón de memento (instantáneas), patrón de comando y difracción de estado. Todos ellos tienen ventajas y desventajas.

Me gustaría ir con State Diffing si puede salirse con la suya, ya que combina la reducción de memoria con facilidad de implementación y mantenimiento.

Tenga en cuenta que el VoxelShop mencionado en el artículo es de código abierto.Para que pueda echar un vistazo a la complejidad del patrón de comandos aquí: https://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history

A continuación se extracto del artículo:

patrón Memento

enter image description here

Pros

  • La implementación es independiente de la acción aplicada. Una vez implementado, podemos agregar acciones sin preocuparnos por romper el historial.
  • Es rápido para avanzar a una posición predefinida en la historia. Esto es interesante cuando las acciones aplicadas entre la posición del historial actual y deseado son computacionalmente costosas.

Contras

  • Requisitos de memoria pueden ser significativamente más alta en comparación con otros enfoques.
  • El tiempo de carga puede ser lento si las instantáneas son grandes.

patrón Comando

enter image description here

Pros

  • huella de memoria es pequeña. Solo tenemos que almacenar los cambios en el modelo y, si son pequeños, la pila de historial también es pequeña.

Contras

  • No podemos ir a una posición arbitraria directamente, sino que necesita un-aplique la pila de la historia hasta llegar allí. Esto puede llevar mucho tiempo.
  • Cada acción y su inversión debe encapsularse en un objeto. Si su acción no es trivial esto puede ser difícil. Los errores en la acción (inversa) son realmente difíciles de depurar y pueden ocasionar fallas fatales. Incluso las acciones de apariencia simple generalmente implican una buena cantidad de complejidad. P.ej. en el caso del Editor 3D, el objeto para agregar al modelo necesita almacenar qué se agregó, qué color se seleccionó actualmente, qué se sobrescribió, si el modo espejo está activo, etc.
  • Puede ser difícil de implementar y requiere mucha memoria cuando las acciones no tiene un simple reverso, por ejemplo, al desenfocar una imagen.

Estado diffing

enter image description here

Pros

  • La implementación es independiente de la acción aplicada. Una vez que se agrega la funcionalidad del historial, podemos agregar acciones sin preocuparnos por romper el historial.
  • Requisitos de memoria suele ser mucho menor que para el enfoque de instantánea y, en muchos casos, comparable con el enfoque de patrón de comando. Sin embargo, esto depende en gran medida del tipo de acciones aplicadas. P.ej. Invertir el color de una imagen usando el patrón de comando debería ser muy barato, mientras que State Diffing salvaría toda la imagen. Por el contrario, al dibujar una línea larga de forma libre, el enfoque de Patrón de comando podría usar más memoria si encadena las entradas del historial para cada píxel.

Contras/Limitaciones

  • No podemos ir a una posición arbitraria directamente, sino que necesita un-aplique la pila de la historia hasta llegar allí.
  • Necesitamos calcular la diferencia entre los estados. Esto puede ser costoso.
  • Implementar la diferencia xor entre los estados del modelo puede ser difícil de implementar dependiendo de su modelo de datos.

Referencia:

https://www.linkedin.com/pulse/solving-history-hard-problem-lukas-siemon

+0

Estos votos negativos sin explicación son realmente desalentadores: / – vincent

Cuestiones relacionadas