Design Pattern per Undo Engine


117

Sto scrivendo uno strumento di modellazione strutturale per un'applicazione di ingegneria civile. Ho un'enorme classe di modelli che rappresenta l'intero edificio, che include raccolte di nodi, elementi di linea, carichi, ecc. Che sono anche classi personalizzate.

Ho già codificato un motore di annullamento che salva una copia completa dopo ogni modifica al modello. Ora ho iniziato a pensare se avrei potuto codificare in modo diverso. Invece di salvare le copie profonde, potrei forse salvare un elenco di ciascuna azione di modifica con un modificatore inverso corrispondente. In modo da poter applicare i modificatori inversi al modello corrente da annullare o i modificatori da ripetere.

Posso immaginare come eseguireste comandi semplici che cambiano le proprietà degli oggetti, ecc. Ma che dire dei comandi complessi? Come l'inserimento di nuovi oggetti nodo nel modello e l'aggiunta di alcuni oggetti linea che mantengono i riferimenti ai nuovi nodi.

Come si potrebbe implementarlo?


Se aggiungo il commento "Undo Algorthim", sarà così che posso cercare "Undo Algorithm" e trovarlo? Questo è quello che ho cercato e ho trovato qualcosa di chiuso come un duplicato.
Peter Turner

fieno, voglio anche sviluppare undo / redo nell'applicazione che stiamo sviluppando. Usiamo il framework QT4 e abbiamo bisogno di molte azioni complesse di annullamento / ripetizione .. Mi chiedevo, hai avuto successo con Command-Pattern?
Ashika Umanga Umagiliya

2
@umanga: ha funzionato ma non è stato facile. La parte più difficile è stata tenere traccia dei riferimenti. Ad esempio, quando un oggetto Frame viene eliminato, i suoi oggetti figlio: Nodi, carichi che agiscono su di esso e molte altre assegnazioni utente dovevano essere conservati per essere reinseriti quando annullati. Ma alcuni di questi oggetti figlio sono stati condivisi con altri oggetti e la logica di annullamento / ripetizione è diventata piuttosto complessa. Se il modello non fosse così grande, manterrei l'approccio del ricordo; è molto più facile da implementare.
Ozgur Ozcitak

questo è un problema divertente su cui lavorare, pensa a come lo fanno i repository di codice sorgente, come svn (mantengono le differenze tra i commit).
Alex

Risposte:


88

La maggior parte degli esempi che ho visto usa una variante del Command-Pattern per questo. Ogni azione dell'utente che è annullabile ottiene la propria istanza di comando con tutte le informazioni per eseguire l'azione e ripristinarla. È quindi possibile mantenere un elenco di tutti i comandi che sono stati eseguiti e ripristinarli uno per uno.


4
Questo è fondamentalmente il modo in cui funziona il motore di annullamento in Cocoa, NSUndoManager.
amrox

33

Penso che sia il ricordo che il comando non siano pratici quando si ha a che fare con un modello delle dimensioni e della portata che l'OP implica. Funzionerebbero, ma sarebbe molto lavoro da mantenere ed estendere.

Per questo tipo di problema, penso che sia necessario creare un supporto al modello di dati per supportare checkpoint differenziali per ogni oggetto coinvolto nel modello. L'ho fatto una volta e ha funzionato molto bene. La cosa più importante che devi fare è evitare l'uso diretto di puntatori o riferimenti nel modello.

Ogni riferimento a un altro oggetto utilizza un identificatore (come un numero intero). Ogni volta che l'oggetto è necessario, si cerca la definizione corrente dell'oggetto da una tabella. La tabella contiene un elenco collegato per ogni oggetto che contiene tutte le versioni precedenti, insieme alle informazioni su quale checkpoint erano attivi.

L'implementazione di undo / redo è semplice: fai la tua azione e stabilisci un nuovo checkpoint; ripristina tutte le versioni degli oggetti al checkpoint precedente.

Ci vuole un po 'di disciplina nel codice, ma ha molti vantaggi: non hai bisogno di copie profonde poiché stai facendo l'archiviazione differenziale dello stato del modello; è possibile definire l'ambito della quantità di memoria che si desidera utilizzare ( molto importante per cose come i modelli CAD) in base al numero di ripristini o alla memoria utilizzata; molto scalabile e con poca manutenzione per le funzioni che operano sul modello in quanto non devono fare nulla per implementare undo / redo.


1
Se si utilizza un database (ad esempio sqlite) come formato di file, questo può essere quasi automatico
Martin Beckett,

4
Se lo aumenti monitorando le dipendenze introdotte dalle modifiche al modello, potresti potenzialmente avere un sistema ad albero di annullamento (cioè se cambio la larghezza di una trave, poi vado a fare un po 'di lavoro su un componente separato, posso tornare indietro e annullare la trave cambia senza perdere il resto). L'interfaccia utente potrebbe essere un po 'ingombrante, ma sarebbe molto più potente di un annullamento lineare tradizionale.
Sumudu Fernando

Puoi spiegare di più l'idea di questo ID contro i puntatori? Sicuramente un puntatore / indirizzo di memoria funziona altrettanto bene dell'id?
paulm

@paulm: essenzialmente i dati effettivi sono indicizzati da (id, version). I puntatori si riferiscono a una particolare versione di un oggetto, ma stai cercando di fare riferimento allo stato corrente di un oggetto, qualunque esso sia, quindi vuoi indirizzarlo per id, non per (id, version). tu potrebbe ristrutturare in modo che si memorizzare un puntatore al (versione => dati) tavolo e basta scegliere l'ultima volta in volta, ma che tende a località danno quando stai persistenza dei dati, infanga riguarda un po ', e lo rende più difficile da fare alcuni tipi di query comuni, quindi non è come sarebbe normalmente.
Chris Morgan

17

Se stai parlando di GoF, il modello Memento si rivolge specificamente all'annullamento.


7
Non proprio, questo affronta il suo approccio iniziale. Chiede un approccio alternativo. L'iniziale memorizza lo stato completo per ogni passaggio mentre il secondo memorizza solo i "diff".
Andrei Rînea

15

Come altri hanno affermato, il modello di comando è un metodo molto potente per implementare Annulla / Ripristina. Ma c'è un vantaggio importante che vorrei menzionare al modello di comando.

Quando si implementa undo / redo utilizzando il modello di comando, è possibile evitare grandi quantità di codice duplicato astrarre (in una certa misura) le operazioni eseguite sui dati e utilizzare tali operazioni nel sistema di annullamento / ripristino. Ad esempio in un editor di testo taglia e incolla sono comandi complementari (a parte la gestione degli appunti). In altre parole, l'operazione di annullamento per un taglio è incolla e l'operazione di annullamento per un incolla viene tagliata. Questo vale per operazioni molto più semplici come la digitazione e l'eliminazione del testo.

La chiave qui è che puoi usare il tuo sistema annulla / ripeti come sistema di comando principale per il tuo editor. Invece di scrivere il sistema come "creare un oggetto di annullamento, modificare il documento" è possibile "creare un oggetto di annullamento, eseguire l'operazione di ripetizione sull'oggetto di annullamento per modificare il documento".

Ora, è vero, molte persone pensano a se stesse "Beh, duh, non fa parte del punto del modello di comando?" Sì, ma ho visto troppi sistemi di comando che hanno due set di comandi, uno per operazioni immediate e un altro impostato per annullare / ripetere. Non sto dicendo che non ci saranno comandi specifici per operazioni immediate e undo / redo, ma ridurre la duplicazione renderà il codice più manutenibile.


1
Non ho mai pensato pastea cut^ -1.
Lenar Hoyt

8

Potresti voler fare riferimento a codice Paint.NET per il loro annullamento: hanno un sistema di annullamento davvero carino. Probabilmente è un po 'più semplice di quello che ti serve, ma potrebbe darti alcune idee e linee guida.

-Adamo


4
In realtà, il codice Paint.NET non è più disponibile, ma è possibile ottenere il biforcuta code.google.com/p/paint-mono
Igor Brejc

7

Questo potrebbe essere un caso in cui CSLA è applicabile. È stato progettato per fornire un supporto di annullamento complesso agli oggetti nelle applicazioni Windows Form.


6

Ho implementato con successo sistemi di annullamento complessi utilizzando il pattern Memento - molto semplice e ha il vantaggio di fornire naturalmente anche un framework Redo. Un vantaggio più sottile è che anche le azioni aggregate possono essere contenute in un singolo Annulla.

In poche parole, hai due pile di oggetti ricordo. Uno per Annulla, l'altro per Ripristina. Ogni operazione crea un nuovo ricordo, che idealmente saranno alcune chiamate per cambiare lo stato del tuo modello, documento (o qualsiasi altra cosa). Questo viene aggiunto allo stack di annullamento. Quando si esegue un'operazione di annullamento, oltre a eseguire l'azione Annulla sull'oggetto Memento per modificare nuovamente il modello, si estrae anche l'oggetto dallo stack Annulla e lo si sposta direttamente sullo stack Ripristina.

Il modo in cui viene implementato il metodo per modificare lo stato del documento dipende completamente dalla tua implementazione. Se puoi semplicemente fare una chiamata API (es. ChangeColour (r, g, b)), fallo precedere da una query per ottenere e salvare lo stato corrispondente. Ma il modello supporterà anche la creazione di copie profonde, snapshot di memoria, creazione di file temporanei, ecc. Dipende tutto da te in quanto è semplicemente un'implementazione del metodo virtuale.

Per eseguire azioni aggregate (ad esempio, l'utente Maiusc-seleziona un carico di oggetti su cui eseguire un'operazione, come eliminare, rinominare, cambiare attributo), il codice crea un nuovo stack di annullamento come un singolo ricordo e lo passa all'operazione effettiva a aggiungere le singole operazioni a. Quindi i metodi di azione non devono (a) avere uno stack globale di cui preoccuparsi e (b) possono essere codificati allo stesso modo sia che vengano eseguiti isolatamente che come parte di un'operazione aggregata.

Molti sistemi di annullamento sono solo in memoria, ma è possibile mantenere lo stack di annullamento se lo si desidera, immagino.


5

Ho appena letto del modello di comando nel mio libro di sviluppo agile - forse ha del potenziale?

Puoi fare in modo che ogni comando implementi l'interfaccia dei comandi (che ha un metodo Execute ()). Se vuoi annullare, puoi aggiungere un metodo Annulla.

maggiori informazioni qui


4

Sono con Mendelt Siebenga sul fatto che dovresti usare il modello di comando. Il modello che hai usato era il modello Memento, che può diventare e diventerà molto dispendioso nel tempo.

Poiché stai lavorando su un'applicazione che richiede molta memoria, dovresti essere in grado di specificare la quantità di memoria che il motore di annullamento può occupare, il numero di livelli di annullamento salvati o lo spazio di archiviazione in cui verranno mantenuti. Se non si esegue questa operazione, si verificheranno presto errori derivanti dall'esaurimento della memoria della macchina.

Ti consiglio di verificare se esiste un framework che ha già creato un modello per gli annullamenti nel linguaggio di programmazione / framework di tua scelta. È bello inventare cose nuove, ma è meglio prendere qualcosa di già scritto, sottoposto a debug e testato in scenari reali. Sarebbe utile se aggiungessi ciò che stai scrivendo, in modo che le persone possano consigliare i framework che conoscono.


3

Progetto Codeplex :

È un semplice framework per aggiungere funzionalità Annulla / Ripristina alle tue applicazioni, basato sul classico schema di progettazione dei comandi. Supporta azioni di fusione, transazioni annidate, esecuzione ritardata (esecuzione su commit di transazione di primo livello) e possibile cronologia di annullamento non lineare (in cui è possibile scegliere tra più azioni da ripetere).


2

La maggior parte degli esempi che ho letto lo fa utilizzando il comando o il pattern memento. Ma puoi farlo anche senza modelli di design con una semplice deque-struttura .


Cosa metteresti nel deque?

Nel mio caso ho inserito lo stato corrente delle operazioni per le quali volevo la funzionalità di annullamento / ripristino. Avendo due deques (annulla / ripeti) annulla la coda di annullamento (apri il primo elemento) e lo inserisco nel ripeti dequeue. Se il numero di elementi nelle dequeues supera la dimensione preferita, apro un elemento della coda.
Patrik Svensson

2
Quello che descrivi in ​​realtà è un modello di design :). Il problema con questo approccio è quando il tuo stato richiede molta memoria - mantenere diverse dozzine di versioni dello stato diventa quindi poco pratico o addirittura impossibile.
Igor Brejc

Oppure è possibile memorizzare una coppia di chiusura che rappresenta l'operazione normale e di annullamento.
Xwtek

2

Un modo intelligente per gestire l'annullamento, che renderebbe il tuo software adatto anche alla collaborazione multiutente, è implementare una trasformazione operativa della struttura dei dati.

Questo concetto non è molto popolare ma ben definito e utile. Se la definizione ti sembra troppo astratta, questo progetto è un esempio riuscito di come una trasformazione operativa per oggetti JSON viene definita e implementata in Javascript



1

Abbiamo riutilizzato il caricamento del file e il codice di serializzazione di salvataggio per "oggetti" per un comodo modulo per salvare e ripristinare l'intero stato di un oggetto. Inseriamo quegli oggetti serializzati nello stack di annullamento, insieme ad alcune informazioni su quale operazione è stata eseguita e suggerimenti sull'annullamento di tale operazione se non ci sono abbastanza informazioni raccolte dai dati serializzati. Undo and Redoing spesso si limita a sostituire un oggetto con un altro (in teoria).

Ci sono stati molti MOLTI bug dovuti a puntatori (C ++) ad oggetti che non sono mai stati riparati mentre si eseguono alcune strane sequenze di annullamento e ripetizione (quei posti non aggiornati per "identificatori" più sicuri che riconoscono l'annullamento). Bug in questa zona spesso ... ummm ... interessanti.

Alcune operazioni possono essere casi speciali di velocità / utilizzo delle risorse, come dimensionare le cose, spostare le cose.

La selezione multipla fornisce anche alcune complicazioni interessanti. Per fortuna avevamo già un concetto di raggruppamento nel codice. Il commento di Kristopher Johnson sui sotto-articoli è molto vicino a quello che facciamo.


Questo sembra sempre più impraticabile con l'aumentare delle dimensioni del tuo modello.
Warren P

In quale modo? Questo approccio continua a funzionare senza modifiche man mano che nuove "cose" vengono aggiunte a ogni oggetto. Le prestazioni potrebbero essere un problema poiché la forma serializzata degli oggetti cresce di dimensioni, ma questo non è stato un grosso problema. Il sistema è in continuo sviluppo da oltre 20 anni ed è utilizzato da migliaia di utenti.
Aardvark

1

Ho dovuto farlo quando scrivevo un risolutore per un gioco di puzzle con salti di pioli. Ho trasformato ogni mossa in un oggetto Command che conteneva informazioni sufficienti per poter essere eseguito o annullato. Nel mio caso è stato semplice come memorizzare la posizione di partenza e la direzione di ogni mossa. Ho quindi memorizzato tutti questi oggetti in uno stack in modo che il programma potesse facilmente annullare tutte le mosse necessarie durante il backtracking.


1

Puoi provare l'implementazione già pronta del pattern Undo / Redo in PostSharp. https://www.postsharp.net/model/undo-redo

Ti consente di aggiungere funzionalità di annullamento / ripristino alla tua applicazione senza implementare il modello da solo. Utilizza il pattern registrabile per tenere traccia delle modifiche nel modello e funziona con il pattern INotifyPropertyChanged, implementato anche in PostSharp.

Ti vengono forniti i controlli dell'interfaccia utente e puoi decidere quale sarà il nome e la granularità di ciascuna operazione.


0

Una volta ho lavorato su un'applicazione in cui tutte le modifiche apportate da un comando al modello dell'applicazione (cioè CDocument ... stavamo usando MFC) sono state mantenute alla fine del comando aggiornando i campi in un database interno mantenuto all'interno del modello. Quindi non abbiamo dovuto scrivere un codice annulla / ripeti separato per ogni azione. Lo stack di annullamento ricorda semplicemente le chiavi primarie, i nomi dei campi e i vecchi valori ogni volta che un record veniva modificato (alla fine di ogni comando).


0

La prima sezione di Design Patterns (GoF, 1994) ha un caso d'uso per l'implementazione di undo / redo come design pattern.


0

Puoi rendere la tua idea iniziale performante.

Usa strutture dati persistenti e mantieni un elenco di riferimenti al vecchio stato . (Ma questo funziona davvero solo se le operazioni tutti i dati nella tua classe di stato sono immutabili e tutte le operazioni su di essa restituiscono una nuova versione --- ma la nuova versione non deve essere una copia completa, basta sostituire la copia delle parti modificate -on-scrittura'.)


0

Ho trovato il modello di comando molto utile qui. Invece di implementare diversi comandi inversi, sto usando il rollback con esecuzione ritardata su una seconda istanza della mia API.

Questo approccio sembra ragionevole se si desidera uno sforzo di implementazione ridotto e una facile manutenibilità (e ci si può permettere la memoria extra per la seconda istanza).

Vedi qui per un esempio: https://github.com/thilo20/Undo/


-1

Non so se ti sarà utile, ma quando ho dovuto fare qualcosa di simile su uno dei miei progetti, ho finito per scaricare UndoEngine da http://www.undomadeeasy.com - un motore meraviglioso e davvero non mi importava molto di quello che c'era sotto il cofano - funzionava e basta.


Pubblica i tuoi commenti come risposta solo se sei sicuro di fornire soluzioni! Altrimenti preferisco postarlo come commento sotto la domanda! (se non lo consente ora! per favore aspetta di ottenere una buona reputazione)
InfantPro'Aravind

-1

A mio parere, UNDO / REDO potrebbe essere implementato in 2 modi in generale. 1. Livello di comando (chiamato livello di comando Annulla / Ripeti) 2. Livello documento (chiamato Annulla / Ripeti globale)

Livello di comando: come molte risposte sottolineano, ciò viene ottenuto in modo efficiente utilizzando lo schema Memento. Se il comando supporta anche la memorizzazione su giornale dell'azione, è facilmente supportata una ripetizione.

Limitazione: una volta che l'ambito del comando è fuori, l'annullamento / ripetizione è impossibile, il che porta all'annullamento / ripetizione a livello di documento (globale)

Immagino che il tuo caso si adatterebbe all'annullamento / ripetizione globale poiché è adatto per un modello che richiede molto spazio di memoria. Inoltre, questo è adatto anche per annullare / ripetere selettivamente. Esistono due tipi primitivi

  1. Tutta la memoria annulla / ripeti
  2. Livello oggetto Annulla Ripeti

In "All memory Undo / Redo", l'intera memoria viene trattata come un dato connesso (come un albero, o un elenco o un grafico) e la memoria è gestita dall'applicazione piuttosto che dal sistema operativo. Quindi gli operatori new e delete se in C ++ sono sovraccaricati per contenere strutture più specifiche per implementare efficacemente operazioni come un file. Se un nodo viene modificato, b. detenere e cancellare i dati ecc., Il modo in cui funziona è fondamentalmente copiare l'intera memoria (assumendo che l'allocazione della memoria sia già ottimizzata e gestita dall'applicazione utilizzando algoritmi avanzati) e memorizzarla in uno stack. Se viene richiesta la copia della memoria, la struttura ad albero viene copiata in base alla necessità di avere una copia superficiale o profonda. Viene eseguita una copia completa solo per quella variabile che viene modificata. Poiché ogni variabile viene allocata utilizzando l'allocazione personalizzata, l'applicazione ha l'ultima parola su quando eliminarla, se necessario. Le cose diventano molto interessanti se dobbiamo partizionare l'Undo / Redo quando accade che abbiamo bisogno di Undo / Redo selettivo e programmatico di un insieme di operazioni. In questo caso, solo a quelle nuove variabili, o variabili cancellate o variabili modificate viene dato un flag in modo che Annulla / Ripeti solo annulli / ripristini quella memoria Le cose diventano ancora più interessanti se dobbiamo fare un Annulla / Ripristina parziale all'interno di un oggetto. In tal caso, viene utilizzata un'idea più recente di "modello visitatore". Si chiama "Annulla / ripeti a livello oggetto" o alle variabili cancellate o alle variabili modificate viene dato un flag in modo che Annulla / Ripristina solo annulli / ripristini quella memoria. Le cose diventano ancora più interessanti se dobbiamo fare un Annulla / Ripristina parziale all'interno di un oggetto. In tal caso, viene utilizzata un'idea più recente di "modello visitatore". Si chiama "Annulla / ripeti a livello oggetto" o alle variabili cancellate o alle variabili modificate viene dato un flag in modo che Annulla / Ripristina solo annulli / ripristini quella memoria. Le cose diventano ancora più interessanti se dobbiamo fare un Annulla / Ripristina parziale all'interno di un oggetto. In tal caso, viene utilizzata un'idea più recente di "modello visitatore". Si chiama "Annulla / ripeti a livello oggetto"

  1. Livello oggetto Annulla / Ripristina: quando viene chiamata la notifica di annullamento / ripristino, ogni oggetto implementa un'operazione di streaming in cui, lo streamer ottiene dall'oggetto i vecchi dati / nuovi dati programmati. I dati che non vengono disturbati vengono lasciati indisturbati. Ogni oggetto riceve uno streamer come argomento e all'interno della chiamata UNDo / Redo, trasmette / scarica i dati dell'oggetto.

Sia 1 che 2 potrebbero avere metodi come 1. BeforeUndo () 2. AfterUndo () 3. BeforeRedo () 4. AfterRedo (). Questi metodi devono essere pubblicati nel comando di base Undo / redo (non il comando contestuale) in modo che anche tutti gli oggetti implementino questi metodi per ottenere un'azione specifica.

Una buona strategia è creare un ibrido di 1 e 2. Il bello è che questi metodi (1 e 2) utilizzano essi stessi schemi di comando

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.