Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
CQRS in Windows Azure
Mark Seemann
Microsoft Windows Azure offre incredibili opportunità e sfide da superare. Alcune opportunità si traducono in una scalabilità elastica, nella riduzione dei costi e in uno sviluppo flessibile. Non mancano tuttavia una serie di sfide, in quanto l'ambiente Windows Azure è diverso da quello degli altri server standard di Windows che ospitano oggi la maggior parte delle applicazioni e dei servizi Microsoft .NET Framework.
Uno degli argomenti più ardui in merito al trasferimento di applicazioni e servizi nel cloud consiste nella scalabilità elastica: è possibile potenziare il servizio in caso di necessità e riportarlo a livelli normali in caso di diminuzione della domanda. In Windows Azure il metodo meno invasivo in questo senso consiste nell'eseguire la scalabilità orizzontale, anziché verticale, aggiungendo più server invece di potenziare quelli esistenti. Per adattarsi a questo modello di scalabilità, un'applicazione deve essere dinamicamente scalabile. In questo articolo viene descritto un approccio efficace alla realizzazione di servizi scalabili e il modo in cui implementarlo in Windows Azure.
Command Query Responsibility Segregation (CQRS) è un nuovo approccio per compilare applicazioni scalabili. Presenta alcune differenze rispetto alla tipologia di architettura .NET a cui siamo abituati, ma si fonda su soluzioni e principi reali e comprovati per conseguire la scalabilità. Sono disponibili numerose informazioni per apprendere il modo in cui compilare sistemi scalabili, ma è anzitutto necessario cambiare forma mentis.
In senso figurato, CQRS non è altro che un'affermazione sulla separazione dei problemi, ma in ambito di architettura software spesso rappresenta un set di modelli correlati. In altre parole, il termine CQRS può assumere due significati: un modello e uno stile architetturale. In questo articolo verranno delineati brevemente entrambi i significati e forniti alcuni esempi basati su un'applicazione Web in esecuzione in Windows Azure.
Informazioni sul modello CQRS
La terminologia sottostante di CQRS ha origine nel linguaggio dei modelli orientati agli oggetti. Un comando è un'operazione che modifica lo stato di un elemento, mentre una query è un'operazione che recupera informazioni sullo stato. A livello più informale, i comandi sono operazioni di scrittura e le query operazioni di lettura.
In base al modello CQRS, le operazioni di scrittura e lettura devono essere esplicitamente modellate come responsabilità segregate. La scrittura e la lettura di dati rappresentano due responsabilità distinte. La maggior parte delle applicazioni necessita di entrambe ma, come illustrato nella Figura 1, ogni responsabilità deve essere trattata separatamente.
Figura 1 Segregazione di operazioni di lettura da operazioni di scrittura
L'applicazione effettua un'operazione di scrittura in un sistema concettuale diverso da quello per le operazioni di lettura.
Ovviamente i dati scritti dall'applicazione saranno disponibili per la lettura. Il modello CQRS non fornisce spiegazioni a riguardo, ma nell'implementazione più semplice possibile i sistemi di lettura e scrittura potrebbero utilizzare lo stesso archivio dati sottostante.
Nell'ambito di questa visione globale le operazioni di lettura e scrittura devono essere rigidamente segregate. Le operazioni di scrittura non restituiscono mai dati. Questa affermazione apparentemente innocua apre a un'ampia gamma di opportunità per creare applicazioni estremamente scalabili.
Stile dell'architettura di CQRS
Oltre al modello CQRS, un elemento di base dello stile dell'architettura di CQRS è rappresentato da un aspetto semplice, ma profondo, sulla visualizzazione dei dati. Osservare la Figura 2, che indica l'interfaccia utente per un'applicazione di prenotazioni, ad esempio un sistema di prenotazioni di un ristorante.
Figura 2 Dati non aggiornati al momento della visualizzazione
Nel calendario vengono mostrate le date in un determinato mese, ma alcune sono disabilitate in quanto già prenotate.
Quanto sono aggiornati i dati in un'interfaccia utente di questo tipo? Nel tempo che intercorre tra la visualizzazione dei dati, il trasferimento in rete, l'interpretazione e l'interazione da parte degli utenti, i dati potrebbero già essere obsoleti nell'archivio sottostante. Maggiore è la quantità di tempo prima che l'utente interagisca, maggiore sarà il livello di obsolescenza dei dati. L'utente potrebbe ad esempio essere interrotto da una telefonata o un altro elemento di distrazione prima di procedere, pertanto il tempo di interazione (ovvero il tempo trascorso da un utente a esaminare una pagina Web) può essere calcolato in minuti.
Un metodo comune per risolvere questo problema consiste nell'utilizzare la concorrenza ottimistica per gestire i casi in cui si verificano conflitti. Gli sviluppatori di applicazioni devono scrivere codice per gestire queste situazioni, ma anziché considerarle casi eccezionali, lo stile dell'architettura di CQRS tratta questa condizione di base. Se i dati sono obsoleti nel momento in cui vengono visualizzati, non devono riflettere i dati nell'archivio centrale. Al contrario, un'applicazione può visualizzare i dati da un'origine di dati denormalizzati dietro l'effettivo archivio dati.
La consapevolezza che i dati visualizzati sono sempre obsoleti, assieme al principio di CQRS in base al quale le operazioni di scrittura non restituiscono mai dati, si traduce in opportunità di scalabilità. Le interfacce non devono attendere la scrittura dei dati, ma possono piuttosto inviare un messaggio asincrono e restituire una visualizzazione per l'utente. I componenti BackgroundWorker ricevono il messaggio e lo elaborano alla velocità stabilità dall'utente. Nella Figura 3 viene illustrato un quadro più completo dell'architettura in stile CQRS.
Figura 3 Architettura in stile CQRS
Ogni volta che l'applicazione deve aggiornare i dati, invia un comando come messaggio asincrono, nella maggior parte dei casi tramite una coda permanente. Appena il comando viene inviato, l'interfaccia può restituire una visualizzazione per l'utente. Un componente BackgroundWorker riceve il messaggio di comando in un processo separato e scrive eventuali modifiche nell'archivio dati. Durante questa operazione genera inoltre un evento come ulteriore messaggio asincrono. Altri gestori di messaggi possono iscriversi a questi eventi e aggiornare di conseguenza una visualizzazione denormalizzata dell'archivio dati.
Sebbene i dati della visualizzazione si trovino dietro ai dati "reali", questa propagazione dell'evento si verifica con una tale rapidità da essere impercettibile per l'utente. Tuttavia, anche se il sistema rallenta a causa di un sovraccarico, i dati rimarranno coerenti.
Questa sorta di architettura può essere implementata in vari sistemi diversi, ma grazie ai suoi concetti espliciti di ruoli di lavoro e code, Windows Azure è un candidato ideale. Tuttavia Windows Azure pone l'utente di fronte a una serie di problematiche in rapporto a CQRS; nella parte rimanente di questo articolo verranno esplorate opportunità e sfide attraverso una semplice applicazione.
Applicazione di prenotazioni
Una semplice applicazione di prenotazioni rappresenta un esempio calzante su come implementare CQRS in Windows Azure. Supponiamo che l'applicazione riceva richieste di prenotazione per un ristorante. La prima pagina visualizzata dall'utente consente di selezionare la data, come illustrato nella Figura 2. Ancora una volta, alcune date sono disabilitate, in quanto già prenotate.
Se l'utente fa clic su una data disponibile, verranno visualizzati un modulo di prenotazione e la relativa ricevuta, come indicato nella Figura 4.
Figura 4 Flusso dell'interfaccia utente della prenotazione
La pagina con la ricevuta tenterà di comunicare all'utente che la prenotazione non è ancora garantita. La decisione finale verrà comunicata tramite posta elettronica.
In CQRS l'interfaccia utente ricopre un ruolo essenziale nella definizione delle aspettative, in quanto l'elaborazione si verifica in background. Tuttavia, durante il normale caricamento, una pagina con ricevuta guadagna tempo a sufficienza. Pertanto, quando l'utente procede, la richiesta è già stata gestita.
Vediamo ora i punti chiave relativi all'implementazione dell'applicazione di prenotazione di esempio. Poiché sono presenti diversi elementi mobili in questa semplice applicazione, affronteremo qui solo i frammenti di codice più significativi. Il codice completo è disponibile nel download allegato a questo articolo.
Invio di comandi
Il ruolo Web viene implementato come applicazione ASP.NET MVC 2. Nel momento in cui l'utente invia il modulo indicato nella Figura 4, verrà richiamata l'azione del controller appropriata:
[HttpPost]
public ViewResult NewBooking(BookingViewModel model)
{
this.channel.Send(model.MakeNewReservation());
return this.View("BookingReceipt", model);
}
Il campo channel è un'istanza inserita di questa semplice interfaccia IChannel:
public interface IChannel
{
void Send(object message);
}
Il comando inviato dal metodo NewBooking attraverso il canale rappresenta i dati del modulo HTML incapsulati in un oggetto Data Transfer. Il metodo MakeNewReservation trasforma semplicemente i dati pubblicati in un'istanza MakeReservationCommand, come illustrato qui:
public MakeReservationCommand MakeNewReservation()
{
return new MakeReservationCommand(this.Date,
this.Name, this.Email, this.Quantity);
}
Poiché il metodo Send restituisce void, l'interfaccia utente può restituire una pagina HTML all'utente nel momento in cui il comando verrà inviato. L'implementazione dell'interfaccia di IChannel basata su una coda garantisce che il metodo Send venga restituito nella massima rapidità.
In Windows Azure possiamo implementare l'interfaccia di IChannel sulla base di code integrate del Servizio di archiviazione Windows Azure. Per inserire i messaggi in questa coda permanente, l'implementazione dovrà serializzare i messaggi. È possibile procedere in vari modi, ma per semplificare ho deciso di utilizzare il serializzatore binario integrato in .NET Framework. In un'applicazione di produzione è tuttavia opportuno considerare alternative, in quanto il serializzatore complica la gestione degli errori relativi al controllo delle versioni. Che cosa accade ad esempio se una nuova versione del codice tenta di deserializzare un BLOB serializzato da una versione precedente? Alcune alternative possibili includono buffer di protocollo, XML o JSON.
Con questo stack di tecnologia l'implementazione di IChannel.Send è semplice:
public void Send(object command)
{
var formatter = new BinaryFormatter();
using (var s = new MemoryStream())
{
formatter.Serialize(s, command);
var msg = new CloudQueueMessage(s.ToArray());
this.queue.AddMessage(msg);
}
}
Il metodo Send serializza il comando e crea un nuovo CloudQueueMessage dall'array di byte risultante. Il campo queue è un'istanza inserita della classe CloudQueue del Windows Azure SDK. Inizializzato con le credenziali e le informazioni dell'indirizzo corretto, il metodo AddMessage aggiunge il messaggio alla coda appropriata. Questa operazione avviene in genere molto rapidamente e pertanto, nel momento in cui viene restituito il metodo, il chiamante può eseguire altre attività. Allo stesso tempo, il messaggio si trova ora nella coda, in attesa di essere prelevato da un processore in background.
Comandi di elaborazione
Mentre i ruoli Web visualizzano normalmente codice HTML e accettano i dati che possono inviare tramite l'interfaccia di IChannel, i ruoli di lavoro ricevono ed elaborano i messaggi dalla coda. Questi oggetti BackgroundWorker sono componenti obsoleti e autonomi. Se pertanto non riescono a mantenersi aggiornati con i messaggi in entrata, è possibile aggiungere ulteriori istanze. È in questo modo che si riesce a conseguire un livello elevato di scalabilità per l'architettura basata su messaggistica.
Come già illustrato, l'invio di messaggi attraverso code di Windows Azure è semplice. È il loro utilizzo in modo sicuro e coerente a rappresentare alcune problematiche. Ogni comando incapsula un'intenzione di modificare lo stato dell'applicazione, pertanto il componente BackgroundWorker deve verificare che non venga perduto alcun messaggio e che i dati sottostanti vengano modificati in modo coerente.
Si tratta di un'operazione piuttosto semplice da verificare per una tecnologia di coda che supporta transazioni distribuite (ad esempio, Accodamento messaggi Microsoft). Le code Windows Azure non sono transazionali, ma presentano un set di garanzie. I messaggi non vengono persi dopo la lettura, ma resi invisibili per un determinato periodo di tempo. I clienti devono estrarre un messaggio dalla coda, eseguire le operazioni appropriate ed eliminare il messaggio come fase conclusiva del processo. In questo consiste il ruolo di lavoro a scopo generico dell'applicazione di prenotazione di esempio, che esegue il metodo PollForMessage illustrato nella Figura 5 in un ciclo infinito.
Figura 5 Metodo PollForMessage
public void PollForMessage(CloudQueue queue)
{
var message = queue.GetMessage();
if (message == null)
{
Thread.Sleep(500);
return;
}
try
{
this.Handle(message);
queue.DeleteMessage(message);
}
catch (Exception e)
{
if (e.IsUnsafeToSuppress())
{
throw;
}
Trace.TraceError(e.ToString());
}
}
Il metodo GetMessage potrebbe restituire null se nella coda non sono presenti messaggi. In questo caso il metodo verrà restituito dopo un'attesa di 500 millisecondi, per poi essere nuovamente richiamato dal ciclo esterno infinito. Alla ricezione di un messaggio, il metodo lo gestisce richiamando il metodo Handle. È in questo punto che si svolge presumibilmente il lavoro vero e proprio. Se pertanto il metodo viene restituito senza generare un'eccezione, è opportuno eliminare il messaggio.
D'altro canto, se si verifica un'eccezione in fase di gestione del messaggio, è necessario eliminare l'eccezione. Un'eccezione non gestita determinerà un arresto anomalo dell'intera istanza di lavoro e preleverà i messaggi dalla coda.
Un'implementazione in produzione deve essere più sofisticata per gestire i cosiddetti messaggi non elaborabili, ma per praticità eviterò di affrontare questo argomento nel codice di esempio.
Se viene elaborata un'eccezione in fase di elaborazione di un messaggio, non verrà eliminato, ma in seguito a un timeout sarà nuovamente disponibile per l'elaborazione. Si tratta di una garanzia fornita dalle code Windows Azure: è possibile elaborare un messaggio almeno una volta. A corollario, può essere replicato varie volte. Tutti i componenti BackgroundWorker devono pertanto essere in grado di gestire repliche di messaggi. È essenziale che tutte le operazioni di scrittura permanenti siano idempotenti.
Operazioni di scrittura idempotenti
Ogni metodo che gestisce un messaggio deve essere in grado di gestire anche le repliche senza compromettere lo stato dell'applicazione. La gestione di MakeReservationCommand è un esempio calzante. Nella Figura 6 è fornita una visione d'insieme del flusso del messaggio.
Figura 6 Flusso di lavoro per la gestione di MakeReservationCommand
Per prima cosa, l'applicazione deve verificare se il ristorante dispone di una capacità sufficiente per la data richiesta. Tutte le tabelle potrebbero essere già prenotate oppure potrebbero essere rimasti liberi ancora alcuni posti. Per rispondere al quesito sulla capacità disponibile, l'applicazione tiene traccia della capacità corrente nell'archivio permanente. A tale scopo sono disponibili diverse opzioni. Una possibilità è rappresentata dal monitoraggio di tutti i dati di prenotazione in un database SQL Azure, ma dal momento che sussistono limiti di dimensioni dei database, un'opzione più scalabile è costituita dall'archiviazione di tabelle o BLOB di Windows Azure.
Nell'applicazione di prenotazione di esempio viene utilizzata l'archiviazione BLOB per archiviare un oggetto Value idempotente serializzato. Questa classe Capacity tiene traccia delle prenotazioni accettate per poter rilevare repliche di messaggi. Per rispondere alla questione sulla capacità rimanente, l'applicazione può caricare un'istanza di Capacity per la data appropriata e richiamare il metodo CanReserve con l'ID di prenotazione corrente:
public bool CanReserve(int quantity, Guid id)
{
if (this.IsReplay(id))
{
return true;
}
return this.remaining >= quantity;
}
private bool IsReplay(Guid id)
{
return this.acceptedReservations.Contains(id);
}
Ogni MakeReservationCommand dispone di un ID associato. Per garantire un comportamento idempotente, la classe Capacity salva ogni ID di prenotazione accettato per poter rilevare repliche. Solo se la chiamata al metodo non è una replica, verrà richiamata la regola business effettiva, per il confronto tra la quantità richiesta e la capacità rimanente.
L'applicazione serializza e archivia un'istanza Capacity per ogni data e pertanto, per sapere se il ristorante dispone di una capacità rimanente, scarica il BLOB e richiama il metodo CanReserve:
public bool HasCapacity(MakeReservationCommand reservation)
{
return this.GetCapacityBlob(reservation)
.DownloadItem()
.CanReserve(reservation.Quantity, reservation.Id);
}
Se la risposta è "true", l'applicazione richiama il set di operazioni associate a questo esito, come indicato nella Figura 6. La prima fase consiste nel ridurre la capacità, richiamando il metodo Capacity.Reserve indicato nella Figura 7.
Figura 7 Metodo Capacity.Reserve
public Capacity Reserve(int quantity, Guid id)
{
if (!this.CanReserve(quantity, id))
{
throw new ArgumentOutOfRangeException();
}
if (this.IsReplay(id))
{
return this;
}
return new Capacity(this.Remaining - quantity,
this.acceptedReservations
.Concat(new[] { id }).ToArray());
}
Ecco un'altra operazione idempotente che richiama i metodi CanReserve e IsReplay come misure di protezione. Se la chiamata al metodo rappresenta una nuova richiesta di prenotare maggiore capacità, verrà restituita una nuova istanza di Capacity con capacità ridotta e l'ID verrà aggiunto all'elenco degli ID accettati.
La classe Capacity è solo un oggetto Value ed è pertanto necessario eseguire nuovamente il commit all'archiviazione BLOB di Windows Azure prima del completamento dell'operazione. Nella Figura 8 viene illustrato il modo in cui il BLOB originale viene inizialmente scaricato dall'archiviazione BLOB di Windows Azure.
Figura 8 Diminuzione della capacità e commit all'archiviazione
public void Consume(MakeReservationCommand message)
{
var blob = this.GetCapacityBlob(message);
var originalCapacity = blob.DownloadItem();
var newCapacity = originalCapacity.Reserve(
message.Quantity, message.Id);
if (!newCapacity.Equals(originalCapacity))
{
blob.Upload(newCapacity);
if (newCapacity.Remaining <= 0)
{
var e = new SoldOutEvent(message.Date);
this.channel.Send(e);
}
}
}
Si tratta dell'istanza di Capacity serializzata corrispondente alla data della prenotazione richiesta. Se la capacità è cambiata (ovvero, non si trattava di una replica), la nuova capacità viene ricaricata nell'archiviazione BLOB.
Che cosa accade se nel frattempo vengono generate eccezioni? Questa situazione potrebbe verificarsi se, ad esempio, l'istanza di Capacity è cambiata dal momento in cui è stato richiamato il metodo CanReserve. Si tratta di una condizione non improbabile in scenari con volumi elevati, in cui vengono gestite contemporaneamente diverse richieste concorrenti. In questi casi il metodo Reserve potrebbe generare un'eccezione, poiché non è disponibile una capacità rimanente sufficiente. Perfetto. Significa che questa richiesta di prenotazione specifica ha perso un concorrente. L'eccezione verrà individuata dal gestore nella Figura 5, ma poiché il messaggio non è stato mai eliminato, verrà visualizzato in un secondo momento per essere nuovamente gestito. In questo caso, il metodo CanReserve restituirà immediatamente false e la richiesta potrà essere tranquillamente rifiutata.
Nella Figura 8 è tuttavia illustrato un altro potenziale conflitto di concorrenza. Che cosa accade se due componenti BackgroundWorker aggiornano la capacità per la stessa data nello stesso momento?
Utilizzo della concorrenza ottimistica
Il metodo Consume nella Figura 8 scarica il BLOB Capacity dall'apposita archiviazione e, se è cambiato, carica un nuovo valore. Molti BackgroundWorker potrebbero effettuare contemporaneamente questa operazione e pertanto l'applicazione dovrà verificare che un valore non sovrascriva l'altro.
Poiché il Servizio di archiviazione Windows Azure è basato su REST, l'utilizzo di tag rappresenta il metodo consigliato per gestire questi problemi di concorrenza. Nel momento in cui l'applicazione crea un'istanza Capacity per una specifica data, ETag sarà null, ma se viene scaricato un BLOB esistente dall'archiviazione, sarà disponibile un valore ETag tramite CloudBlob.Properties.ETag. Se l'applicazione carica l'istanza Capacity, dovrà impostare l'elemento AccessCondition corretto in un'istanza di BlobRequestOptions:
options.AccessCondition = etag == null ?
AccessCondition.IfNoneMatch("*") :
AccessCondition.IfMatch(etag);
Se l'applicazione crea una nuova istanza di Capacity, ETag è null e AccessCondition deve essere impostato su IfNoneMatch("*"). In questo modo verrà generata un'eccezione se il BLOB è già presente. Di contro, se l'operazione di scrittura corrente rappresenta un aggiornamento, AccessCondition deve essere impostato su IfMatch, per garantire la generazione di un'eccezione nel caso in cui ETag nell'archiviazione BLOB non corrisponda all'ETag fornito.
La concorrenza ottimistica basata su ETag costituisce uno strumento essenziale, ma è necessario abilitarlo in modo esplicito fornendo l'elemento BlobRequestOptions appropriato.
Se in fase di riduzione della capacità non viene generata alcuna eccezione, l'applicazione può passare alla fase successiva nella Figura 6: la scrittura della prenotazione nell'archiviazione tabelle, che segue più o meno gli stessi principi alla base della riduzione della capacità e pertanto non verrà trattata in questo articolo. Il codice è disponibile nel download allegato, ma ancora una volta è essenziale sottolineare che l'operazione di scrittura dovrà essere idempotente.
L'ultima fase nel flusso di lavoro consiste nel generare un evento indicante l'accettazione della prenotazione. A tale scopo viene inviato un altro messaggio asincrono tramite la coda Windows Azure. Questo evento di dominio potrà essere selezionato e gestito da qualsiasi altro BackgroundWorker interessato. Un'azione rilevante consiste nell'inviare un messaggio di posta elettronica all'utente, ma l'applicazione dovrà anche chiudere il ciclo per l'interfaccia utente aggiornando l'archivio dati di visualizzazione.
Aggiornamento dei dati di visualizzazione
Gli eventi che si verificano durante l'elaborazione di un comando vengono inviati come messaggi asincroni tramite l'interfaccia IChannel. Il metodo Consume nella Figura 8 genera ad esempio un nuovo evento SoldOutEvent se la capacità è ridotta a zero. Altri gestori di messaggi possono iscriversi a questi eventi per aggiornare di conseguenza i dati di visualizzazione, come illustrato di seguito:
public void Consume(SoldOutEvent message)
{
this.writer.Disable(message.Date);
}
L'operazione di scrittura inserita implementa il metodo Disable aggiornando un array di date disabilitate per il mese appropriato nell'archiviazione BLOB:
public void Disable(DateTime date)
{
var viewBlob = this.GetViewBlob(date);
DateTime[] disabledDates = viewBlob.DownloadItem();
viewBlob.Upload(disabledDates
.Union(new[] { date }).ToArray());
}
In questa implementazione viene semplicemente scaricato un array di istanze DateTime disabilitate dall'archiviazione BLOB, la nuova data viene aggiunta all'array e viene nuovamente caricata. Poiché si utilizza il metodo Union, l'operazione è idempotente e ancora una volta il metodo Upload incapsula la concorrenza ottimistica basata su ETag.
Query dei dati di visualizzazione
L'interfaccia utente può ora eseguire query direttamente dai dati di visualizzazione. Si tratta di un'operazione efficace, in quanto i dati sono statici e non è richiesto alcun calcolo. Per aggiornare ad esempio la selezione di date nella Figura 2 con date disabilitate, viene inviata una richiesta AJAX al controller per ottenere l'array.
Il controller può semplicemente gestire la richiesta in questo modo:
public JsonResult DisabledDays(int year, int month)
{
var data = this.monthReader.Read(year, month);
return this.Json(data, JsonRequestBehavior.AllowGet);
}
L'operazione di lettura inserita implementa il metodo Read leggendo il BLOB scritto dal gestore dell'evento SoldOutEvent:
public IEnumerable<string> Read(int year, int month)
{
DateTime[] disabledDates =
this.GetViewBlob(year, month).DownloadItem();
return (from d in disabledDates
select d.ToString("yyyy.MM.dd"));
}
Il ciclo viene chiuso. L'utente esplora il sito in base ai dati di visualizzazione correnti e compila un modulo per inviare i dati gestiti tramite messaggistica asincrona. I dati di visualizzazione vengono infine aggiornati in base agli eventi di dominio generati durante il flusso di lavoro.
Denormalizzazione dei dati
Ricapitolando, la maggior parte delle applicazioni legge molti più dati di quanti ne scriva e pertanto la scalabilità viene garantita ottimizzando le operazioni di lettura, soprattutto se i dati possono essere letti da risorse statiche, quali i BLOB. I dati visualizzati in una schermata sono sempre disconnessi, ovvero diventeranno obsoleti dal momento in cui vengono visualizzati. CQRS comprende questa obsolescenza disconnettendo la lettura e la scrittura dei dati. I dati letti non devono necessariamente provenire dalla stessa origine dei dati scritti. Possono anzi essere trasportati in modo asincrono dall'archivio in cui sono scritti ad archivi specifici della visualizzazione in cui il costo di progettazione e manipolazione viene corrisposto solo una volta.
Con le sue code e i suoi archivi dati denormalizzati e scalabili, Windows Azure rappresenta un candidato ideale per questo tipo di architettura. Sebbene le transazioni distribuite non siano supportate, le code garantiscono che i messaggi non vengano mai perduti, ma serviti almeno una volta. Per gestire potenziali repliche, tutte le operazioni di scrittura asincrone devono essere idempotenti. Per dati denormalizzati quali le archiviazioni BLOB e di tabelle, è necessario utilizzare ETag per implementare la concorrenza ottimistica. Grazie a queste semplici tecniche viene garantita una coerenza futura.
In questo articolo sono stati forniti solo brevi cenni su CQRS. Per ulteriori informazioni, sono disponibili varie risorse di approfondimento in Internet; un valido punto di partenza è tuttavia rappresentato dalla pagina iniziale su CQRS di Rinat Abdullin, all'indirizzo abdullin.com/cqrs.
Mark Seemann è responsabile tecnico di Windows Azure per Commentor A/S, una società di consulenze danese con sede a Copenhagen. È l'autore di "Dependency Injection in .NET" (Manning Publications, 2011) e il creatore del progetto open source AutoFixture. Il suo blog è disponibile all'indirizzo blog.ploeh.dk.
Un ringraziamento ai seguenti esperti tecnici per la revisione dell'articolo: Rinat Abdullin e Karsten Strøbæk