Il presente articolo è stato tradotto automaticamente.

Smart Client

Creazione di applicazioni distribuite con NHibernate e Rhino Service Bus, Parte 2

Oren Eini

Di luglio 2010 numero di MSDN Magazine, ho iniziato l'esame del processo di creazione di un'applicazione smart client per una libreria di percentuale. Ho definito progetto Alexandria e ha deciso di utilizzare NHibernate per l'accesso ai dati e Rhino Service Bus di comunicazione affidabile con il server.

NHibernate (nhforge.org ) è un framework di mapping relazionale a oggetti (O/RM) e Rhino Service Bus (github.com/rhino-esb/rhino-esb ) è un'implementazione di bus del servizio open source basata su Microsoft .NET Framework. Eseguita profondamente essere coinvolti nello sviluppo di tali Framework in modo che sembrava l'opportunità di implementare un progetto con tecnologie conosce intimately, allo stesso tempo forniscono un esempio funzionante per gli sviluppatori che desiderano conoscere NHibernate e Rhino Service Bus.

Nell'articolo precedente, ho parlato di blocchi predefiniti di base dell'applicazione smart client. Ho progettato il back end, con la modalità di comunicazione tra applicazioni smart client e il server back-end. Toccato anche in modalità batch e memorizzazione nella cache, la gestione delle transazioni e la sessione NHibernate, come utilizzare e rispondere ai messaggi dal client e come tutto ciò che viene fornito assieme nel programma di avvio automatico.

In questo numero illustrerà le procedure consigliate per l'invio di dati tra server back-end e smart client dell'applicazione, nonché modelli per la gestione delle modifiche distribuite. Lungo il percorso, si parlerà rimanenti dettagli di implementazione e presenterà un client per l'applicazione di Alexandria completato.

È possibile scaricare la soluzione di esempio Alexandria di github.com/ayende/. La soluzione è costituito da tre parti: Alexandria.Backend ospita il codice back-end Alexandria.Client contiene codice front-end; Alexandria.Messages contiene le definizioni di messaggi condivise tra di essi.

Nessuno regole di modello

Uno dei più comuni domande persone porsi durante la scrittura di applicazioni distribuite è: Come è possibile inviare le entità per l'applicazione client e quindi applicare la modifica impostate sul lato server?

Se la domanda, probabilmente pensando in una modalità in cui il lato server è principalmente un repository di dati. Se tali applicazioni sono scelte tecnologia che può rendere semplificano questa attività (ad esempio employing WCF RIA Services e WCF Data Services). Utilizzando il tipo di architettura che ho descritto finora, tuttavia non ha molto senso parlare di entità l'invio in rete. In realtà, l'applicazione di Alexandria utilizza tre modelli distinti per gli stessi dati, ogni modello più adatto per le diverse parti dell'applicazione.

Il modello di dominio sul back-end, che viene utilizzato per l'interrogazione e l'elaborazione delle transazioni, è adatto per l'utilizzo con NHibernate e ulteriore perfezionamento sarebbe suddividere le responsabilità di elaborazione di query e transazioni. Il modello di messaggio rappresenta i messaggi in transito, inclusi alcuni concetti strettamente associati alle entità del dominio (BookDTO nel progetto di esempio è un duplicato dei dati della Rubrica). Nell'applicazione client, il modello di visualizzazione (ad esempio la classe BookModel) è ottimizzato da associare al codice XAML e gestire le interazioni dell'utente.

Sebbene a prima vista è possibile visualizzare molti commonalities tra tre modelli (Book, BookDTO, BookModel), significa che il fatto che hanno diverse responsabilità tentando cram tutte in un unico modello dovrebbe creare un modello complicato, heavyweight, uno a dimensione-non-adatta a chiunque. Suddividendo il modello lungo le linee delle responsabilità apportate al lavoro molto più semplice poiché è possibile ridefinire ogni modello indipendentemente per adattarlo ai propri fini.

Da un punto di vista concettuale, esistono altri motivi per creare un modello separato per ogni utilizzo. Un oggetto è una combinazione di dati e il comportamento, ma quando si tenta di inviare un oggetto in rete, l'unica cosa che è possibile inviare è nei dati. Che comporta alcune domande interessanti. Eseguire posizione logica aziendale che deve essere eseguita sul server back-end? Se si inseriscono nelle entità, cosa succede se si esegue questa logica sul client?

Il risultato finale di questo tipo di architettura è che non si utilizzano gli oggetti reali. Invece, si utilizzano gli oggetti dati, oggetti che sono semplicemente che contiene i dati, e la logica business si trova altrove, come le procedure eseguite su dati dell'oggetto. Al momento, questo è frowned perché genera dispersione logica e il codice è più difficile da gestire nel tempo. Indipendentemente come si osserva, a meno che il sistema back-end è un repository di dati semplici, si dispone di diversi modelli in parti diverse dell'applicazione. Che, naturalmente, porta a una domanda molto interessante: come prevede di gestire le modifiche?

Comandi di insiemi di modifiche

Tra le operazioni di consentire agli utenti dell'applicazione di Alexandria siano aggiungendo libri relativa coda riordinamento delle rubriche in coda e rimozione dalla coda, come illustrato in di Figura 1. Tali operazioni devono riflessi nel front end e back end.

image: Possible Operations on the User’s Books Queue

Figura 1 operazioni possibili nella documentazione dell'utente coda

Si potrebbe tenta di implementare questa serializzazione le entità in rete e inviando l'entità modificata al server per la persistenza. In realtà, NHibernate contiene supporto esplicito per solo tali scenari utilizzando il metodo session.Merge.

Tuttavia, let’s presuppongono la seguente regola di business: Quando un utente aggiunge un libro alla propria coda nell'elenco di consigli, quel libro viene rimosso da raccomandazioni e viene aggiunta un'altra indicazione.

Immaginiamo tentando di rilevare che un libro è stato spostato nell'elenco di consigli alla coda utilizzando semplicemente il precedente e corrente stato (l'insieme di modifiche tra i due stati). Mentre può essere eseguita, è un understatement dire che sarebbe difficile da gestire.

È possibile chiamare tali architetture Trigger-Oriented Programming. Come trigger in un database, cosa in un sistema basato sul set di modifiche è codice riguarda principalmente dati. Per fornire alcune semantica aziendale significativo, è necessario estrarre il significato delle modifiche da modifica imposta forza bruta e fortuna.

C'è un motivo che contenente la logica del trigger viene considerato un anti-modello. Sebbene appropriate per alcune operazioni (quali operazioni dati puri o replica) tentativo di implementare la logica di business con i trigger è un processo complicato che conduce a un sistema che è difficile da gestire.

La maggior parte dei sistemi che espongono un'interfaccia CRUD e consentono di scrivere regole business in metodi quali UpdateCustomer sono fornendo Trigger-Oriented programmazione predefinita (e in genere l'unica scelta disponibile). Quando non c'è significativo logica coinvolta, quando il sistema nel suo complesso è principalmente sui CRUD, questo tipo di architettura ha senso, ma nella maggior parte delle applicazioni non è appropriato e non consigliata.

Al contrario, un'interfaccia esplicita (RemoveBookFromQueue e AddBookToQueue, ad esempio) in un sistema è molto più semplice da comprendere e pensare. La possibilità di scambiare informazioni a questo livello alto consente un notevole grado di libertà e facile modifica la strada verso il basso. Dopo tutto, si Don ’t dover scoprire in cui alcune funzionalità del sistema si basa su quali dati vengono manipolati da tale funzionalità. Il sistema verrà compitare esattamente dove ciò avviene basata sulla sua architettura.

L'implementazione di Alexandria segue il principio di interfaccia esplicita, richiamare le operazioni risiede nel modello di applicazione e illustrato in di Figura 2. Sono eseguendo diverse cose interessanti qui, così let’s gestire ciascuno di essi in ordine.

Figura 2 aggiunta una Rubrica alla coda dell'utente dal front end

public void AddToQueue(BookModel book) {
  Recommendations.Remove(book);
  if (Queue.Any(x => x.Id == book.Id) == false) 
    Queue.Add(book);

  bus.Send(
    new AddBookToQueue {
      UserId = userId, BookId = book.Id
    },
    new MyQueueQuery {
      UserId = userId
    },
    new MyRecommendationsQuery {
      UserId = userId
    });
}

Innanzitutto, è possibile modificare il modello dell'applicazione direttamente per riflettere immediatamente desideri l'utente. È possibile effettuare quanto l'aggiunta di che una Rubrica alla coda dell'utente è un'operazione che viene mai avere esito negativo. È possibile inoltre rimuovere dall'elenco suggerimenti, perché non ha senso avere una voce nella coda dell'utente inoltre visualizzato nell'elenco dei suggerimenti.

Successivamente, inviare un batch di messaggi al server back-end, indicando per aggiungere il manuale coda dell'utente e Consenti conoscono l'utente coda e raccomandazioni dopo questa modifica. Si tratta di un concetto importante da comprendere.

La possibilità di scrivere comandi e query in questo modo significa Don ’t eseguire operazioni speciali nei comandi come AddBookToQueue per ottenere i dati modificati per l'utente. Invece il front end può chiedere come parte del messaggio stesso batch e funzionalità esistente consente di ottenere i dati.

Esistono due motivi, che è possibile richiedere i dati dal server back-end anche se è possibile apportare le modifiche in memoria. Innanzitutto, il server back-end può eseguire logica aggiuntiva (ad esempio ricerca nuove indicazioni per l'utente) che comporta modifiche dubbi sul lato server front-end. In secondo luogo, la risposta dal server back-end aggiornerà la cache con lo stato corrente.

Gestione locale stato disconnesso

Si può notare un problema di Figura 2 riguardo al lavoro disconnesso. Le modifiche apportate in memoria, ma fino a quando non è possibile ottenere una risposta dal server, i dati memorizzati nella cache non verranno per riflettere tali modifiche. In caso di riavvio dell'applicazione mentre ancora disconnesso, l'applicazione visualizzerà informazioni scadute. Riprende la comunicazione con il server back-end, i messaggi sarebbero flusso al back-end e dovrebbe risolvere lo stato finale previsto della quale l'utente. Ma fino a quel momento, le applicazioni vengono visualizzate informazioni che l'utente ha già modificato localmente.

Per le applicazioni che prevedono lunghi periodi di disconnessione, Don ’t basarsi solo nella cache del messaggio; invece di implementare un modello ha mantenuto dopo ogni operazione dell'utente.

Per l'applicazione di Alexandria esteso le convenzioni della cache scada immediatamente qualsiasi informazione che fa parte di un batch di query del comando messaggio come quello in di Figura 2. In questo modo, è possibile non disporrà di informazioni aggiornate, ma anche non mostrerò errata informazioni se l'applicazione viene riavviata prima è possibile ottenere una risposta dal server back-end. Ai fini dell'applicazione Alexandria, è sufficiente.

Elaborazione back-end

Ora che comprendere il funzionamento del processo sul lato server front-end cose, let’s esaminare il codice dal punto di vista del server back-end. Si ha già familiarità con la gestione delle query, illustrato nell'articolo precedente. Figura 3 Mostra il codice per la gestione di un comando.

Nella figura 3 Aggiunta di una Rubrica in coda dell'utente

public class AddBookToQueueConsumer : 
  ConsumerOf<AddBookToQueue> {

  private readonly ISession session;

  public AddBookToQueueConsumer(ISession session) {
    this.session = session;
  }

  public void Consume(AddBookToQueue message) {
    var user = session.Get<User>(message.UserId);
    var book = session.Get<Book>(message.BookId);

    Console.WriteLine("Adding {0} to {1}'s queue",
      book.Name, user.Name);

    user.AddToQueue(book);
  }
}

Il codice effettivo è poco interessante. È possibile caricare le entità rilevanti e quindi chiama un metodo sull'entità per eseguire l'attività effettiva. Tuttavia, è più importante di quanto si pensi. Potrebbe sostenere, è processo di un architetto, per essere certi che gli sviluppatori nel progetto come verificarsi possibili. La maggior parte dei problemi aziendali sono interessante e rimuovendo complessità tecnologica dal sistema, è possibile ottenere una percentuale maggiore di sviluppatore tempo impiegato per lavorare su problemi aziendali interessante anziché interessanti problemi tecnologici.

Cosa significa nel contesto di Alexandria? Anziché la distribuzione logica aziendale in tutti i consumer di messaggi, ho centralizzata la maggior parte della logica aziendale possibili nelle entità. Idealmente, consumando un messaggio segue questo modello:

  • Caricare i dati necessari per l'elaborazione del messaggio
  • Chiamare un metodo unico di un'entità di dominio per eseguire l'operazione effettiva

Questo processo assicura che la logica di dominio verrà rimangono nel dominio. Come per i quali tale logica è, Beh, che consiste di scenari è necessario gestire. Dovrebbe dare un'idea di come gestisce la logica di dominio in caso di User.AddToQueue(book):

public virtual void AddToQueue(Book book) {
  if (Queue.Contains(book) == false)
    Queue.Add(book);
  Recommendations.Remove(book);

  // Any other business logic related to 
  // adding a book to the queue
}

Si è visto un caso in cui la logica di front-end e la logica di back-end corrispondono esattamente. Ora let’s osservare un caso in cui essi Don ’ t. Rimozione di una Rubrica dalla coda è molto semplice in primo piano terminare (vedere di Figura 4). È piuttosto semplice. Rimuovere la Rubrica dalla coda locale (che rimuove dall'interfaccia utente), quindi inviare un batch di messaggi al server back-end, chiedendo a rimuovere la Rubrica dalla coda e aggiornare la coda e le raccomandazioni.

Figura 4 rimozione di una Rubrica dalla coda di

public void RemoveFromQueue(BookModel book) {
  Queue.Remove(book);

  bus.Send(
    new RemoveBookFromQueue {
      UserId = userId,
      BookId = book.Id
    },
    new MyQueueQuery {
      UserId = userId
    },
    new MyRecommendationsQuery {
      UserId = userId
    });
}

Sui server back-end che utilizzano il messaggio RemoveBookFromQueue segue lo schema illustrato in Figura 3 caricando le entità e chiamando il metodo user.RemoveFromQueue(book):

public virtual void RemoveFromQueue(Book book) {
  Queue.Remove(book);
  // If it was on the queue, it probably means that the user
  // might want to read it again, so let us recommend it
  Recommendations.Add(book);
  // Business logic related to removing book from queue
}

Il comportamento è diverso tra il front-end e back-end. Sul back-end, aggiungo il libro rimosso le raccomandazioni, non sul front-end. Quale sarebbe il risultato della disparità?

Bene, risposta immediata, è possibile rimuovere la Rubrica dalla coda, ma non appena le risposte dal server back-end di raggiungere il front end, si vedrà il libro aggiunto all'elenco dei suggerimenti. In pratica, probabilmente sarà possibile notare la differenza solo se il server back-end è stato arrestato quando si rimuove un libro dalla coda.

Che è tutto molto interessante, ma cosa quando è effettivamente necessaria conferma dal server back-end per completare un'operazione?

Operazioni complesse

Quando l'utente deve aggiungere, rimuovere o riordinare elementi nella propria coda, è abbastanza ovvio che l'operazione non può avere esito negativo, in modo da consentire l'applicazione di accettare immediatamente l'operazione. Ma per operazioni quali la modifica indirizzi o cambiare la carta di credito, non è possibile semplicemente accettare l'operazione fino a ottenere una conferma della riuscita dal backend.

In Alexandria, questo viene implementato come un processo in quattro fasi. Suoni con tema horror ma è davvero semplice. Figura 5 illustra le fasi possibili.

image: Four Possible Stages for a Command Requiring Confirmation

Figura 5 di quattro fasi possibili per un comando di richiesta di conferma

Nella schermata in alto a sinistra viene illustrata la visualizzazione normale dei dettagli sottoscrizione. È come Alexandria Mostra confermate le modifiche. Nella parte inferiore sinistra schermata mostra la schermata Modifica i dati. Facendo clic su Salva il pulsante in questa schermata risultati nella schermata illustrata nella top–right; si tratta come Alexandria Mostra non confermate le modifiche .

In altre parole, è possibile accettare la modifica (temporaneamente) fino a quando non è possibile ottenere una risposta dal server che indica che la modifica è stata accettata (che riporta noi sullo schermo in alto a sinistra) o rifiutato, che sposta il processo alla schermata nella parte inferiore destra. Tale schermata viene visualizzato un errore dal server e consente di correggere il dettaglio errato.

L'implementazione non è complesso, nonostante si potrebbe pensare. Verrà avviato nel back-end e sposta verso l'esterno. Figura 6 viene visualizzato il codice di back-end necessario per gestire questa e non è un valore nuovo. Ho state effettuando più la stessa cosa in questo articolo. La maggior parte delle funzionalità del comando condizionale (e complessità) si trova nel front end.

Figura 6 di Gestione back-end di modifica indirizzo dell'utente

public void Consume(UpdateAddress message) {
  int result;
  // pretend we call some address validation service
  if (int.TryParse(message.Details.HouseNumber, out result) == 
    false || result % 2 == 0) {
    bus.Reply(new UpdateDetailsResult {
      Success = false,
      ErrorMessage = "House number must be odd number",
      UserId = message.UserId
    });
  }
  else {
    var user = session.Get<User>(message.UserId);
    user.ChangeAddress(
      message.Details.Street,
      message.Details.HouseNumber,
      message.Details.City, 
      message.Details.Country, 
      message.Details.ZipCode);

    bus.Reply(new UpdateDetailsResult {
      Success = true,
      UserId = message.UserId
    });
  }
}

Una cosa diversa dalla visto prima è che qui è necessario codice esplicito successo o meno per l'operazione, mentre in precedenza semplicemente richiesto un aggiornamento dei dati in una query separata. Un errore di operazione possibile e si desidera conoscere non solo se l'operazione ha esito positivo, ma perché non è riuscito.

Alexandria viene utilizzato il Caliburn framework per gestire gran parte drudgery della gestione dell'interfaccia utente. Caliburn (caliburn.codeplex.com ) è un framework WPF/Silverlight si basa principalmente sulle convenzioni semplificano la generazione gran parte delle funzionalità dell'applicazione nel modello di applicazione anziché scrivere codice nel codice XAML sottostante.

As you’ll see from looking at the sample code, just about everything in the Alexandria UI is wired via the XAML using conventions, giving you both clear and easy to understand XAML and an application model that directly reflects the UI without having a direct dependency on it. This results in significantly simpler code.

Figure 7 should give you an idea about how this is implemented in the SubscriptionDetails view model. In essence, SubscriptionDetails contains two copies of the data; one is held in the Editable property and that’s what all the views relating to editing or displaying unconfirmed changes show. The second is held in the Details property, which is used to hold the confirmed changes. Each mode has a different view, and each mode selects from which property to display the data.

Figure 7 Moving Between View Modes in Response to User Input

public void BeginEdit() {
  ViewMode = ViewMode.Editing;

  Editable.Name = Details.Name;
  Editable.Street = Details.Street;
  Editable.HouseNumber = Details.HouseNumber;
  Editable.City = Details.City;
  Editable.ZipCode = Details.ZipCode;
  Editable.Country = Details.Country;
  // This field is explicitly ommitted
  // Editable.CreditCard = Details.CreditCard;
  ErrorMessage = null;
}

public void CancelEdit() {
  ViewMode = ViewMode.Confirmed;
  Editable = new ContactInfo();
  ErrorMessage = null;
}

In the XAML, I wired the ViewMode binding to select the appropriate view to show for every mode. In other words, switching the mode to Editing will result in the Views.SubscriptionDetails.Editing.xaml view being selected to show the edit screen for the object.

It is the save and confirmation processes you will be most interested in, however. Here’s how I handle saving:

public void Save() {
  ViewMode = ViewMode.ChangesPending;
  // Add logic to handle credit card changes
  bus.Send(new UpdateAddress {
    UserId = userId,
    Details = new AddressDTO {
      Street = Editable.Street,
      HouseNumber = Editable.HouseNumber,
      City = Editable.City,
      ZipCode = Editable.ZipCode,
      Country = Editable.Country,
    }
  });
}

The only thing I’m actually doing here is sending a message and switching the view to a non-editable one with a marker saying that those changes have not yet been accepted. Figure 8 shows the code for confirmation or rejection. All in all, a miniscule amount of code to implement such a feature, and it lays the foundation for implementing similar features in the future.

Figure 8 Consuming the Reply and Handling the Result

public class UpdateAddressResultConsumer : 
  ConsumerOf<UpdateAddressResult> {
  private readonly ApplicationModel applicationModel;

  public UpdateAddressResultConsumer(
    ApplicationModel applicationModel) {

    this.applicationModel = applicationModel;
  }

  public void Consume(UpdateAddressResult message) {
    if(message.Success) {
      applicationModel.SubscriptionDetails.CompleteEdit();
    }
    else {
      applicationModel.SubscriptionDetails.ErrorEdit(
        message.ErrorMessage);
    }
  }
}

//from SubscriptionDetails
public void CompleteEdit() {
  Details = Editable;
  Editable = new ContactInfo();
  ErrorMessage = null;
  ViewMode = ViewMode.Confirmed;
}

public void ErrorEdit(string theErrorMessage) {
  ViewMode = ViewMode.Error;
  ErrorMessage = theErrorMessage;
}

You also need to consider classic request/response calls, such as searching the catalog. Because communication in such calls is accomplished via one-way messages, you need to change the UI to indicate background processing until the response from the back-end server arrives. I won’t go over that process in detail, but the code for doing it exists in the sample application.

Checking Out

At the beginning of this project, I started by stating the goals and challenges I anticipated facing in building such an application. The major challenges I intended to address were data synchronization, the fallacies of distributed computing, and handling an occasionally connected client. Looking back, I think Alexandria does a good job of meeting my goals and overcoming the challenges.

The front-end application is based on WPF and making heavy use of the Caliburn conventions to reduce the actual code for the application model. The model is bound to the XAML views and a small set of front-end message consumers that make calls to the application model.

I covered handling one-way messaging, caching messages at the infrastructure layer and allowing for disconnected work even for operations that require back-end approval before they can really be considered complete.

On the back end, I built a message-based application based on Rhino Service Bus and NHibernate. I discussed managing the session and transaction lifetimes and how you can take advantage of the NHibernate first-level cache using messages batches. The message consumers on the back end serve either for simple queries or as delegators to the appropriate method on a domain object, where most of the business logic actually resides.

Forcing the use of explicit commands rather than a simple CRUD interface results in a clearer code. This allows you to change the code easily, because the entire architecture is focused on clearly defining the role of each piece of the application and how it should be built. The end result is a very structured product, with clear lines of responsibility.

È difficile tentare di ottenere indicazioni per un'architettura completa dell'applicazione distribuita in alcuni articoli brevi, soprattutto durante il tentativo di introdurre nuovi concetti diversi nello stesso momento. Comunque, penso che troverete che applicando le procedure qui descritte determinerà in applicazioni che sono effettivamente più lavorare con le architetture basate su RPC o CRUD più tradizionali.

Oren Eini (Chi opera sotto la Ayende pseudonimo Rahien) è un membro attivo di diversi progetti open source (NHibernate e castello tra di essi) ed è il fondatore di molti altri (Rhino Mocks, NHibernate Query Analyzer e Commons Rhino tra di essi). Eini è inoltre responsabile del profiler NHibernate ( nhprof.com ), un debugger visual per NHibernate. È possibile seguire il suo lavoro ayende.com/Blog.