...un po' di storia: tanto tempo fa in una galassia lontana lontana... comiciai a scrivere applicazioni gestionali Windows Forms... mano a mano che il mondo evolveva mi resi conto che, forse addirittura più nel nostro lavoro che in altri, l'abito fa eccome il monaco. Con questo intendo dire che spessissimo il giudizio ultimo sul nostro lavoro lo danno "non tecnici" che si limitano a valutare le funzionalità di alto livello che il nostro prodotto offre. Da questo ritengo abbastanza facile dedurre che l'interfaccia utente, e qui credo di non dire nulla di nuovo, ricopra un ruolo fondamentale perchè è il con la UI che il nostro giudice ha il primo approccio.
Fatta questa doverosa premessa veniamo a quello che successe: un bel giorno una delle ragazze che lavoravano in contabilità da un cliente mi chiese (parlando dell'applicazione che avevo sviluppato e che lei utilizzava): "ma non ci sarebbe la possibilità di avere l'avanti e indiero come in Word?" la pima reazione fu un grosso punto di domanda... avanti e indietro?? ma che è... indagando poi scoprii che la ragazza si riferiva alle funzionalità di Undo/Redo offerte da Office.
La cosa mi incuriosì non poco e comincia a studiare il "meccanismo" (aka "pattern") che gestiva la cosa e la prima, ed unica veramente problematica, considerazione a cui giunsi era che per Word la cosa era "relativamente semplice" perchè Word sapeva molto bene cosa stava gestendo, e cioè il documento, mentre la mia velleità era quella di realizzare un motore che fosse in grado di maneggiare un generico grafo di oggetti.
La prima implementazione di tutto vide la luce qualche anno fa, e la ragazza della contabilità fu felice, realizzata con il fx 1.0. Era qualcosa di decisamente embrionale erano le stesse entities che tenevano traccia delle variazioni (o meglio solo della prima variazione) che subivano:
public string Company
{
get { return this._company; }
set
{
if(base.ObjectCache["Company"] == null)
base.ObjectCache["Company"] = this._company;
this._company = value;
}
}
Avevo semplicemente un dictionary dove il nome della proprietà era la chiave e il value il valore, veniva cachata solo ed esclusivamente la prima modifica e nulla di più. L'entity esponeva poi metodi e proprietà per giocare con la cache:
- AcceptChanges();
- AcceptChanges( Boolean recursive );
- RejectChanges();
- RejectChanges( Boolean recursive );
- GetHasChanges();
- GetHasChanges( Boolean recursive );
I metodi con il parametro boolean permettevano di capire se l'intero grafo avesse subito modifiche o se le modifiche erano limitate al solo oggetto che si stava indagando. Il tutto naturalmente funzionava anche per le collection anche se la manutenibilità del codice era decisamente lontana dal potersi considerare accettabile.
Adesso mi vengono i brividi ;-) ma 6 e più anni fa faceva la sua porca figura... il tempo e le necessità mi hanno fatto evolvere verso qualcosa di decisamente più funzionale, e finalmente oggi, anche molto più bello dal punto di vista del design.
Partiamo dai requisiti che possono essere sintetizzati in un una parola: "Memento" (che non è solo un bellissimo film).
I problemi:
- gestiamo un grafo non necessariamente noto a priori;
- gestiamo un grafo che può essere internamente gestito con il "lazy loading" e non vogliamo che il modello venga caricato tutto solo perchè dobbiano gestirne le variazioni di stato;
- vogliamo poter distinguere n track line per gestire in contemporanea grafi differenti senza che le modifiche vengano mischiate: faccio prima a fare un esempio che ha spiegare: se in VS fate modifiche su file diversi tutte le modifiche vengono tracciate ma, anche se le fate un po' qua e un po' la in maniera casuale, quando fate Undo viene fatta la cosa giusta sul file giusto ma soprattutto se fate Undo su un documento n volte vengono ripristinate solo le modifiche a quello specifico file anche se durante l'editing le avete mischiate, in quanto ad ordine, a quelle di un altro file.
Nel tempo sono passato attraverso varie implementazioni di un motore di caching dello stato di una entity, ho scritto anche un articolo che spiega (stringendo molto) quello che ho adesso in produzione, e seppur tutte funzionanti avevano un grosso difetto: delegavano alla stessa entity l'onere di fare cache del suo stato, non che ritenga la cosa così grave anche perchè il motore, che è tutt'ora usato, è decisamente smart e copre circa il 90% delle casistiche che ho incontrato sino a questo momento.
Il framework 2.0 introduce 2 nuove interfacce IChangeTracking e IRevertibleChangeTracking, peraltro inutilizzate dal fx stesso, che servono per gestire proprio un motore di caching. La cosa fuorviante in questo caso è MSDN stessa che, per IChangeTracking ad esempio, recita:
"Defines the mechanism for querying the object for changes and resetting of the changed status."
Lasciando supporre che sia la entity a dover fornire/gestire queste informazioni.
In questo periodo ci si stava ponendo però un potenziale scenario in cui avremmo sprecato un sacco di risorse (cicli CPU, non ore/uomo) inultilmente proprio perchè la gestione dello stato era delegata alla stessa entity. Non mi dilungo sui motivi, sto già scrivendo troppo oggi... ;-) avremo l'occasione per parlarne presto.... e chi ha orecchie per intendere intenda.
Dato questo incipit in questo week-end diluviante mi sono messo d'impegno e ho cercato di capire cosa potevo fare, la prima cosa è stato come sempre guardare il resto del mondo: in molti usano l'interfaccia INotifyPropertyChanged, o meccanismi simili proxando l'interfaccia pubblica di una entity, ma devo dire che a me piace veramente poco:
- obbliga/permette di gestire le sole proprietà pubbliche;
- il valore cachato è necessariamente quello esposto mentre potrebbe essere necessario customizzare questo processo;
- in fase di "rollback/undo" è decisamente complesso capire, dall'interno della entity, che il set di una property è dovuto al rollback e non ad altro e questo è necessario per evitare di triggerare ancora il motore di caching portando a ricorsione;
- va ancora ancora benino finchè stiamo tentando di tracciare le modifche ad una singola entity molto semplice, la classica Person, ma se pensiamo ad un grafo complesso, ad esempio un Customer con Orders e relativi OrderItems e Products, Addresses ed altro l'interfaccia INotifyPropertyChanged è decisamente inadeguata;
La soluzione a cui sono giunto è ancora un po' grezza ma perfettamente funzionante e decisamente più lineare dal punto di vista del design. Come al solito mi sono ispirato a quello che gia c'è, e di cui ho anche scritto, in questo caso System.Transactions.TransactionScope:
using( TransactionScope ts = new TransactionScope() )
{
//Do something
ts.Complete();
}
qualsiasi operazione eseguiamo o oggetto istanziamo, all'interno del blocco using, è in grado di eseguire l'enlistment nella transazione in maniera automatica senza che ci sia passaggio di alcunchè tra il blocco di codice di esempio e gli oggetti che vengono usati, a qualsiasi livello si trovino. Altra cosa interessante è che se la transazione non c'è, perchè ad esempio non abbiamo creato un oggetto TransactionScope, il codice funziona perfettamente... e non è che sia poi così scontato ;-)
Quello a cui sono giunto è questo:
IChangeTrackingServiceProvider provider = ChangeTrackingServiceProvider.GetCurrent();
provider.CreateTrackingService();
IList<Person> list = new EntityCollection<Person>();
Person p = new Person();
p.FirstName = "Mauro";
p.LastName = "Servienti";
list.Add( p );
IChangeTrackingService svc = provider.GetTrackingService();
if( svc.IsChanged )
{
svc.RejectChanges();
}
Perchè il tutto funzioni non è necessario implementare nessuna interfaccia sulle entity che si stanno realizzando è però necessario sacrivere un minimo di codice per "collegare" i 2 mondi. Nell'esempio per la collection viene utilizzata un EntityCollection<T> che è una mia classe, che implementa IList<T> (e non solo), da cui è possibile derivare e che sgravia l'utilizzatore/inheritor da tutto il plumbing code necessario per la gestione del tracking delle modifiche all'interno di una lista (cosa non proprio banale). Mentre la classe Person deriva da una mia classe Entity e si limita ad 1 chiamata ad un metodo della classe base, nel caso in cui ci si volesse totalmente sganciare dalla mia implementazione è sempre possibile farlo, anche in maniera parziale, perchè tutto è implementato da classi concrete che derivano da classi astratte (che offrono una implementazione di base) ma che vengono sempre esposte sotto forma di interfacce: è quindi possibile, in maniera decisamente semplice, pluggare un proprio modello e personalizzare pesantemente il tutto.
Ecco quello che succede in esecuzione:
Non uso un blocco using, anche se IChangeTrackingService implementa IDisposable, perchè il ciclo di vita della entity, e quindi la sua gestione dello stato tendono ad andare oltre lo scope del singolo snippet di codice. Ogni metodo esposto dall'interfaccia IChangeTrackingServiceProvider ha svariati overload che permettono di raffinare/personalizzare il comportamento del motore di caching. Una volta recuparata uan reference all'IChangeTrackingService corrente oltre ad accettare o rifiutare (AcceptChanges() o RejectChanges()) è possibile eseguire un "Undo" progressivo delle modifiche apportate, banalmente una cosa del tipo:
while( svc.IsChanged )
{
svc.Undo();
}
E' infine possibile chiedere all'IChangeTrackingService di restituirci un IChangeSet che è l'insieme delle modifiche che sta tracciando.
IChangeSet cSet = svc.GetChangeSet();
Il metodo GetChangeSet() ha, per ora, un overload che accetta un IChangeSetFilter che è un oggetto che permette di "filtrare" dall'esterno quali modifiche debbano essere incluse nel changeset che si sta costruendo. Questa possibilità è ancora molto grezza ed è legata ad una possibile collaborazione con qualcosa che implementi il pattern "Unit Of Work" (vedasi NSK per i dettagli) al fine di poter scrivere una cosa del tipo:
using( IUnitOfWork uow = IServiceContainer.GetService<IUnitOfWork>() )
{
uow.Append( cSet );
uow.Commit();
cSet.AcceptChanges();
}
Anche se già funzionante la sua implementazione interna lascia molto a desiderare.
Lascio ad un futuro post i dettagli implementativi del tutto... per oggi ho già intasato troppo il muro di UGI.
.m