MokaByte Numero 29  - Aprile 1999   

Memento

di
Lorenzo Bettini

Come realizzare meccanismi di undo


 


Spesso nelle applicazioni è necessario mettere a disposizione meccanismi per permettere di tornare sui propri passi; altre volte è necessario "fotografare" lo stato di certi oggetti. Questo pattern permette proprio di realizzare soluzioni efficaci e riusabili a questi problemi. 


Introduzione

Chi ha fatto latino alle superiori, sa che memento significa ricordare; in effetti il compito di questo pattern è proprio quello di tenere memoria delle varie azioni che vengono svolte da un determinato oggetto. Si pensi ad un'applicazione che offre interazione con l'utente (basta prendere un normalissimo text editor, word processor, o un programma di grafica); in questi programmi, o comunque in quelli seri, viene sempre offerta la possibilità di tornare sui propri passi disfacendo (undo) l'azione appena fatta.

Questa possibilità può essere più o meno utile, basandosi sulla granularità delle azioni considerate; senz'altro risulterebbe molto più utile avere a disposizione un meccanismo di undo multilivello, cioè la possibilità di poter annullare più azioni (anche contemporaneamente). Il programma che mette a disposizione un tale meccanismo si deve mantenere memorizzata una sorta di history dei vari stati del documento, in modo da poter riportare il tutto ad una situazione precedente; ovviamente è comodissimo avere anche la possibilità di disfare quello che abbiamo disfatto, o molto più semplicemente di avere un meccanismo di redo (anche questo presente nei programmi che mettono a disposizione il meccanismo di undo).

Altre volte è necessario semplicemente tenere memorizzati dei checkpoint degli stati di certi oggetti, anche in questo caso per implementare dei meccanismi di roll back (si veda ad esempio le transazioni atomiche nei DBMS). Si deve però trovare un metodo efficiente e non dispendioso in fatto di memoria e risorse di tenere memorizzata questa history; senz'altro non si può pensare di tenere in memoria (anche secondaria) una copia del documento per ogni azione eseguita. Vedremo come il pattern memento risolve in modo pratica ed elegante (caratteristica ormai appurata dei pattern) questo tipo di problema.

Il problema

Il problema principale consiste dunque nel memorizzare lo stato di un certo oggetto, senza però violare l'incapsulazione: di solito solo l'oggetto stesso può accedere direttamente ai propri dati; e quindi un oggetto esterno non è in grado di accedere allo stato dell'oggetto interessato, per salvarlo.

L'idea di base della soluzione è quella di utilizzare un altro oggetto (che chiameremo appunto memento), il cui scopo principale è quello di memorizzare lo stato di un altro oggetto (che chiameremo creatore del memento). L'oggetto che si occuperà del meccanismo di undo dovrà richiedere al creatore (che ricordiamo è soprattutto l'oggetto di cui vogliamo tenere memorizzati i vari stati), un oggetto memento, al momento del checkpoint; il creatore creerà a quel punto un oggetto memento inzializzandolo con le informazioni necessarie (e sufficienti) a ripristinare tale stato.

Al momento che il meccanismo di undo entrerà in azione dovrà semplicemente passare il memento al creatore, che lo utilizzerà per riportare se stesso allo stato in cui era stato creato il memento. Notare che a soluzione del memento disaccoppia il meccanismo di undo dai vari oggetti che dovranno supportare tale meccanismo: il memento non lascia trasparire lo stato dell'oggetto memorizzato ed il meccanismo di undo non ha accesso allo stato dell'oggetto osservato.

Il pattern

Formalizziamo adesso i partecipanti al pattern (che senz'altro si contraddistingue per la sua semplicità), mantenendo i nomi inglesi (onde evitare traduzioni un pò abbozzate):

  • Memento: è l'oggetto che si occupa di tenere memorizzato lo stato di un oggetto (l'Originator); tale oggetto nasconde lo stato dell'oggetto agli oggetti di altre classi; gli altri oggetti hanno accesso solo ad un'interfaccia povera del memento: in effetti gli altri oggetti lo possono solo passare all'oggetto che deve ripristinare il suo stato. Ovviamente, invece, l'originator dovrà avere accesso a tutti i dati memorizzati nel memento che gli serviranno per ripristinare il proprio stato.
  • Originator: è l'oggetto di cui si vuole tenere memorizzato lo stato, ed è quello che si occupa di creare i vari memento, coi dati caratteristici del proprio stato in quel momento; inoltre utilizzerà anche l'oggetto memento per ritornare allo stato memorizzato nel memento.
  • Caretaker: è l'oggetto che implementa il meccanismo di undo; si occupa di richiedere un oggetto memento all'originator, a tenere memorizzati i vari memento e di passarli all'originator, al momento che è necessario applicare il meccanismo di undo.

Tipicamente un oggetto memento sarà caratterizzato da due metodi: GetState e SetState, il cui significato dovrebbe essere abbastanza chiaro: l'Originator chiamerà SetState al momento della creazione del memento per inizializzarlo con tutti i dati necessari; quando poi riceverà un memento (in tal caso è stato messo in esecuzione il meccanismo di undo), chiamerà GetState per ottenere le informazioni sullo stato che aveva precedentemente memorizzato nel memento. Una sorta di interazione fra i vari oggetti può essere riassunta nel seguente diagramma di interazione:

Caretaker   Originator   Memento
CreateMemento() ----> new Memento() ---->  
    SetState() ---->  
... ... ... ...  
SetMemento(m) ----> GetState() ---->  

Come sempre vi lasci adesso ad Andrea Trentini che vi mostrerà come in pratica può essere implementato e soprattutto utilizzato un memento.

A presto :-)

Lorenzo