Un po' di codice C++
//
// La classe TrOp contiene una operazione che puo'
// essere annullata durante un rollback.
// Si tratta di una classe base astratta da cui
// derivano tutti i tipi di operazione annullabile.
//
class TrOp
{
public:
virtual void Undo() = 0;
virtual ~TrOp() {}
};
//
// Il template Change genera al variare del tipo
// dati usato le possibili operazioni (derivate
// da TrOp) che e' possibile annullare in un
// rollback. In pratica l'istruzione
//
// new Change<int>(a);
//
// crea un oggetto "cambio di intero" e salva
// il vecchio valore in modo da rendere
// annullabile l'operazione eseguita.
//
template<class T> class Change : public TrOp
{
public:
T& who;
T old_value;
Change(T& s) : who(s), old_value(s) { }
void Undo() { who=old_value; }
};
//
// Il vettore Transaction contiene tutte le
// operazioni da annullare in caso di una
// richiesta di rollback.
//
std::vector<TrOp *> Transaction;
//
// Il template field genera al variare del
// tipo dati i possibili campi di un oggetto.
// Il campo provvede a generare un opportuno
// oggetto Change<tipo> ad ogni modifica.
//
template<class T> class Field : public T
{
private:
void Save()
{
Transaction.push_back(
new Change<T>(*static_cast<T*>(this))
);
}
Field& operator&();
public:
// Costruttore da oggetto T
Field(const T& s) : T(s) { }
// Conversione a valore (const!!!)
operator const T() const { return *this; }
// Assegnazione
Field& operator=(const T& v)
{ Save(); T::operator=(v); return *this; }
Field& operator=(const Field<T>& v)
{ Save(); T::operator=(v); return *this; }
// Rimappatura ++/--
const T operator++(int)
{ Save(); return T::operator++(0); }
const T operator--(int)
{ Save(); return T::operator--(0); }
Field& operator++()
{ Save(); T::operator++(); return *this; }
Field& operator--()
{ Save(); T::operator--(); return *this; }
// Rimappatura incrementi/decrementi
Field& operator+=(const T& v)
{ Save(); T::operator+=(v); return *this; }
Field& operator-=(const T& v)
{ Save(); T::operator-=(v); return *this; }
Field& operator*=(const T& v)
{ Save(); T::operator*=(v); return *this; }
Field& operator/=(const T& v)
{ Save(); T::operator/=(v); return *this; }
Field& operator%=(const T& v)
{ Save(); T::operator%=(v); return *this; }
//
// Nota: se sono presenti metodi che
// modificano una classe e' necessario
// inserire un opportuno override in modo
// da effettuare una Save() completa o
// Save() specifiche delle parti modificate
// prima di invocare il metodo in questione.
//
}; |
|
Queste dichiarazioni mi hanno permesso di definire
le classi contenenti i dati gestionali in termini
di Field<...>e di ottenere a costo zero(1) la logica di rollback sulla
rappresentazione in memoria.
Uno dei problemi della soluzione e' quello che
per ogni modifica viene creato un nuovo oggetto
di tipo Change<tipo> e questo comporta un
inutile spreco (infatti in caso di rollback solo
il valore originale precedente la prima modifica
e' realmente necessario).
Io ho deciso di non occuparmi del problema perche'
con la soluzione attuale non esiste un overhead
di spazio per i campi; in altre parole un campo
di tipo Field<int> occupa esattamente quanto
un campo di tipo int . Inoltre la soluzione
corrente permette anche la gestione di transazioni
nidificate con rollback parziali(2) .
La classe template Field<class T>e' stata dichiarata
come derivata della classe T, in modo da ottenere un
automatico "dirottamento" di tutte le chiamate a metodi
di Field<T>verso i corrispondenti metodi di T. Quello
che pero' e' importante notare e' che tutte le operazioni
che modificano l'oggetto devono essere precedute da una
chiamata di Save() che provvede a generare il necessario
oggetto Change<>corrispondente.
Nel template vengono incapsulati salvataggi prima di tutti
gli operatori che normalmente modificano l'oggetto (come
assegnazione, pre/post incremento o +=). Se sono presenti
altri metodi che modificano l'oggetto questi devono essere
indicati nel template(3) .
Inoltre, visto che non e' possibile derivare dai tipi base,
una specializzazione di Field<>e' necessaria per tutti
i tipi nativi (int, long, float, ...) e per i puntatori.
| | |
(1) | Ovviamente esiste un costo affatto trascurabile in
termini di prestazioni e occupazione di memoria. In
altre parole la modifica di un Field<>porta ad
operazioni aggiuntive di copia/allocazione che ne
rallentano l'esecuzione. Con costo zero intendo un
costo nullo dal punto di vista della logica di
implementazione delle transazioni.
| (2) | Non ho sentito per ora la necessita' di implementare
la cosa ma molto semplicemente utilizzare un puntatore
NULL all'interno di Transaction mi permetterebbe di
indicare il punto in cui fermare una richiesta di
rollback.
| (3) | La soluzione che ho trovato non e' affatto generale; tuttavia
e' in grado di risolvere egregiamente il problema di strutture
dati formate da puntatori e campi relativamente "stupidi" come
si comportino in maniera molto simile ai tipi nativi (es.
std::string). Questa soluzione non e' facilmente estendibile
al caso di oggetti complessi memorizzati in Field<>.
|
|