Pergunta

Preciso implementar o trabalho de estrutura Desfazer/Refazer para meu aplicativo de janela (editor como o PowerPoint), qual deve ser a melhor prática a seguir, como seria lidar com todas as alterações de propriedades dos meus objetos e seu reflexo na interface do usuário.

Foi útil?

Solução

Existem dois padrões clássicos para usar.O primeiro é o padrão de lembrança que é usado para armazenar instantâneos do estado completo do seu objeto.Talvez isso exija mais sistema do que o padrão de comando, mas permite a reversão de maneira muito simples para um instantâneo mais antigo.Você pode armazenar os instantâneos em disco como PaintShop/PhotoShop ou mantê-los na memória para objetos menores que não requerem persistência.O que você está fazendo é exatamente para o que esse padrão foi projetado, portanto, ele deve se adequar um pouco melhor do que o padrão de comando sugerido por outros.

Além disso, uma observação adicional é que, como não exige que você tenha comandos recíprocos para desfazer algo que foi feito anteriormente, isso significa que quaisquer funções potencialmente unilaterais [como hash ou criptografia] que não podem ser desfeitas trivialmente usando recíproco os comandos ainda podem ser desfeitos simplesmente revertendo para um instantâneo mais antigo.

Também como apontado, o padrão de comando que consome potencialmente menos recursos, por isso admitirei que em casos específicos em que:

  • Há um grande estado de objeto a ser persistido e/ou
  • Não existem métodos destrutivos e
  • Onde comandos recíprocos podem ser usados ​​de forma muito trivial para reverter qualquer ação tomada

o padrão de comando poderia ser mais adequado [mas não necessariamente, vai depender muito da situação].Em outros casos, eu usaria o padrão memento.

Eu provavelmente evitaria usar um mashup dos dois porque tendo a me preocupar com o desenvolvedor que virá atrás de mim e manterá meu código, além de ser minha responsabilidade ética para com meu empregador tornar esse processo tão simples e barato quanto possível.Vejo um mashup dos dois padrões se tornando facilmente um buraco de rato insuportável e de desconforto cuja manutenção seria cara.

Outras dicas

A prática clássica é seguir o Padrão de comando.

Você pode encapsular qualquer objeto que execute uma ação com um comando e faça com que ele execute a ação reversa com um método undo (). Você armazena todas as ações em uma pilha para uma maneira fácil de retroceder através delas.

Existem três abordagens aqui que são viáveis.Padrão Memento (instantâneos), padrão de comando e diferenciação de estado.Todos eles têm vantagens e desvantagens.

Eu escolheria o State Diffing se você conseguir se safar, pois combina redução de memória com facilidade de implementação e manutenção.

Observe que o VoxelShop mencionado no artigo é de código aberto.Então você pode dar uma olhada na complexidade do padrão de comando aqui:https://github.com/simlu/voxelshop/tree/develop/src/main/java/com/vitco/app/core/data/history

Abaixo está um trecho do artigo:

Padrão de lembrança

enter image description here

Prós

  • A implementação é independente da ação aplicada.Uma vez implementado, podemos adicionar ações sem nos preocupar em quebrar o histórico.
  • É rápido avançar para uma posição predefinida na história.Isto é interessante quando as ações aplicadas entre a posição histórica atual e desejada são computacionalmente caras.

Contras

  • Os requisitos de memória podem ser significativamente maiores em comparação com outras abordagens.
  • O tempo de carregamento pode ser lento se os instantâneos forem grandes.

Padrão de comando

enter image description here

Prós

  • O consumo de memória é pequeno.Precisamos apenas armazenar as alterações no modelo e, se forem pequenas, a pilha de histórico também será pequena.

Contras

  • Não podemos ir diretamente para uma posição arbitrária, mas sim cancelar a aplicação da pilha de histórico até chegarmos lá.Isso pode ser demorado.
  • Cada ação e seu inverso precisam ser encapsulados em um objeto.Se sua ação não for trivial, isso pode ser difícil.Erros na ação (reversa) são realmente difíceis de depurar e podem facilmente resultar em falhas fatais.Mesmo ações aparentemente simples geralmente envolvem uma boa quantidade de complexidade.Por exemplo.no caso do Editor 3D, o objeto a ser adicionado ao modelo precisa armazenar o que foi adicionado, qual cor foi selecionada no momento, o que foi sobrescrito, se o modo espelho está ativo etc.
  • Pode ser difícil de implementar e consumir muita memória quando as ações não têm um simples reverso, por exemplo, ao desfocar uma imagem.

Diferença de estado

enter image description here

Prós

  • A implementação é independente da ação aplicada.Depois que a funcionalidade do histórico for adicionada, podemos adicionar ações sem nos preocupar em quebrar o histórico.
  • Os requisitos de memória geralmente são muito mais baixos do que os da abordagem Snapshot e, em muitos casos, comparáveis ​​à abordagem Command Pattern.No entanto, isso depende muito do tipo de ações aplicadas.Por exemplo.inverter a cor de uma imagem usando o Padrão de Comando deveria ser muito barato, enquanto a Diferença de Estado salvaria a imagem inteira.Por outro lado, ao desenhar uma longa linha de formato livre, a abordagem Command Pattern pode usar mais memória se encadear entradas de histórico para cada pixel.

Contras / Limitações

  • Não podemos ir diretamente para uma posição arbitrária, mas sim cancelar a aplicação da pilha de histórico até chegarmos lá.
  • Precisamos calcular a diferença entre os estados.Isso pode ser caro.
  • A implementação da diferença xor entre os estados do modelo pode ser difícil de implementar, dependendo do seu modelo de dados.

Referência:

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

Dê uma olhada no Padrão de comando. Você precisa encapsular todas as alterações no seu modelo em objetos de comando separados.

Eu escrevi um sistema realmente flexível para acompanhar as mudanças. Eu tenho um programa de desenho que implementa 2 tipos de alterações:

  • Adicionar/remover uma forma
  • mudança de propriedade de uma forma

Classe 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();
}

Classe derivada para adicionar 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;
    }
}

Classe derivada para remover 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;
    }
}

Classe derivada para alterações de propriedade:

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;
        }
    }
}

Com cada vez Vormen = A matriz de itens que são enviados à alteração. E deve ser usado assim:

Declaração das pilhas:

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

Adicionando uma nova forma (por exemplo, ponto)

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

Removendo uma forma selecionada

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

Registrando uma mudança de propriedade

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

Finalmente a ação de desfazer/refazer

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

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

Eu acho que essa é a maneira mais eficaz de implementar um algoritmo de desfazer. Para um exemplo, veja esta página no meu site: Desenhe isso.

Eu implementei o material de desfazer refazer em torno da linha 479 do arquivo tekening.cs. Você pode baixar o código -fonte. Pode ser implementado por qualquer tipo de aplicação.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top