Este artigo foi traduzido por máquina.

Cliente inteligente

Criando aplicativos distribuídos com o NHibernate e o Rhino Service Bus, Parte 2

Oren Eini

O de de edição de julho 2010 da MSDN Magazine, iniciei percorrendo o processo de criação de um aplicativo cliente inteligente para uma biblioteca lending. Eu chamado de projeto Alexandria e decidiu usar o NHibernate para comunicação confiável com o servidor de acesso a dados e barramento de serviços de Rhino.

O NHibernate (nhforge.org ) é uma estrutura de mapeamento relacional de objeto (S/RM) e barramento de serviços de Rhino (github.com/rhino-esb/rhino-esb ) é uma implementação de barramento de serviço do código-fonte aberto criada no Microsoft .NET Framework. Eu acontecer profundamente estar envolvidos no desenvolvimento de ambos dessas estruturas para que ele parecia ser a oportunidade de se implementar um projeto com as tecnologias intimamente, sei que, ao mesmo tempo fornecem um exemplo funcional para desenvolvedores que desejam aprender sobre NHibernate e barramento de serviços de Rhino.

No artigo anterior, abordei os blocos de construção básicos do aplicativo cliente inteligente. Projetei o back-end, juntamente com o modo de comunicação entre o aplicativo smart client e o back-end. Eu também alterado no cache, como gerenciar transações e a sessão do NHibernate, como consumir e responder às mensagens do cliente e como tudo o que se encaixam no bootstrapper e processamento em lotes.

Neste artigo, abordarei as práticas recomendadas para o envio de dados entre o smart client e o back-end do aplicativo, bem como padrões para o gerenciamento de alterações distribuídos. Ao longo do caminho, eu abordarei os detalhes restantes sobre a implementação e apresentará um cliente concluído para o aplicativo Alexandria.

Você pode baixar a solução de exemplo do alexandria https://github.com/ayende/alexandria . A solução consiste em três partes: Alexandria.Backend hospeda o código de back-end Alexandria.Client contém o código de front-end; Alexandria.Messages contém as definições de mensagem compartilhadas entre eles.

Nenhum um modelo de regras

Uma das pessoas de perguntas mais comuns perguntar quando estiver criando aplicativos distribuídos: Como enviar minhas entidades para o aplicativo cliente e, em seguida, aplicar a alteração que definir no lado do servidor?

Se essa for sua pergunta, você deve estar pensando em um modo em que o lado do servidor é principalmente um repositório de dados. Se você criar esses aplicativos, existem opções de tecnologia que você pode fazer com que simplificam a tarefa (por exemplo, empregando o WCF RIA Services e os serviços de dados do WCF). Usando o tipo de arquitetura que descrevi até agora, no entanto, não realmente faz sentido para falar sobre o envio de entidades no meio físico. Na verdade, o aplicativo Alexandria usa três modelos diferentes para os mesmos dados de cada modelo práticas adequado para diferentes partes do aplicativo.

O modelo de domínio no back-end, que é usado para consultar e processamento transacional, é adequado para uso com o NHibernate (e ainda mais refinamento seria dividir as responsabilidades de processamento transacional e de consulta). O modelo de mensagem representa as mensagens durante a transmissão, incluindo conceitos de mapeiam atentamente a entidades de domínio (BookDTO no projeto de exemplo é um clone de dados do catálogo). No aplicativo cliente, o modelo de modo de exibição (como a classe BookModel) é otimizado para ser ligados ao XAML e manipular as interações do usuário.

Embora à primeira vista, você pode ver muitas semelhanças entre os três modelos (catálogo, BookDTO, BookModel), significa que o fato de que eles terão responsabilidades diferentes tentando cram todos eles em um único modelo seria criar um modelo complicado, heavyweight, qualquer uma-tamanho-não-ajuste-um. Dividindo o modelo ao longo das linhas de responsabilidades, eu fiz o trabalho muito mais fácil porque refine cada modelo de maneira independente para ajustar suas próprias finalidades.

Do ponto de vista conceitual, há outras razões para criar um modelo separado para cada uso. Um objeto é uma combinação de dados e o comportamento; quando você tenta enviar um objeto durante a transmissão, a única coisa que você pode enviar os dados. Isso leva a algumas questões interessantes. Faz local lógica de negócios deve ser executado no servidor back-end? Se você colocá-lo nas entidades, o que acontece se você executar essa lógica no cliente?

O resultado final desse tipo de arquitetura é que não esteja usando objetos reais. Em vez disso, você está usando os objetos de dados — objetos simplesmente estão mantendo os dados — e a lógica de negócios reside em outro lugar, como procedimentos que são executados sobre os dados do objeto. Frowned, porque isso leva a dispersão de lógica e o código é mais difícil de manter-se ao longo do tempo. Não importa como você vê-la, a menos que o sistema back-end é um repositório de dados simples, que você deseja ter modelos diferentes em partes diferentes do aplicativo. Isso, claro, leva a uma pergunta muito interessante: como você vai lidar com as alterações?

Comandos de conjuntos de alteração

Entre as operações permitem que os usuários do aplicativo Alexandria estão adicionando livros para a fila a reordenação de livros na fila e removê-los por completo da fila, conforme mostrado no do Figura 1. É necessário que essas operações sejam refletidas no front-end e back-end.

Figura 1 de fila possíveis operações nos livros do usuário

Eu poderia tentar implementar isso serialização de entidades durante a transmissão e enviando-a entidade modificada de volta para o servidor para persistência. Na verdade, NHibernate contém suporte explícito apenas tais cenários, o uso do método session.Merge.

No entanto, suponha let’s a regra de negócios a seguir: Quando um usuário adiciona um catálogo para a fila da lista de recomendações, esse livro é removido das recomendações e outra recomendação é adicionada.

Imagine tentar detectar que um livro foi movido na lista de recomendações para a fila usando apenas o anterior e o atual estado (conjunto de alterações entre os dois estados). Pode ser feito, quer dizer que seria inconveniente lidar com é um understatement.

Eu chamo essas arquiteturas Trigger-Oriented Programming. Como disparadores no banco de dados, o que você tem em um sistema baseado em conjuntos de alterações é código que lida principalmente com os dados. Para fornecer a semântica de negócios significativas, você precisa extrair o significado das alterações de alteração definida por força bruta e sorte.

Há um motivo que os disparadores que contém a lógica são considerados um antipadrão. Embora adequado para algumas coisas (como, por exemplo, as operações de dados puros ou duplicação), a tentativa de implementar a lógica de negócios usando disparadores é um processo trabalhoso que leva a um sistema que é difícil de manter.

A maioria dos sistemas que expõem uma interface CRUD e permitem que você escrever a lógica de negócios de métodos como UpdateCustomer estão oferecendo Trigger-Oriented de programação como o padrão (e, em geral, a única opção). Quando a lógica de negócios significativas não envolvidos — quando o sistema como um todo é principalmente sobre CRUD — esse tipo de arquitetura faz sentido, mas na maioria dos aplicativos, ele não é adequado e não é recomendado.

Em vez disso, uma interface explícita (RemoveBookFromQueue e AddBookToQueue, por exemplo) resulta em um sistema é muito mais fácil de entender e pensar. Permite que a capacidade de trocar informações com esse nível alto, um ótimo grau de liberdade e a modificação fácil no futuro. Afinal, você Don precisa descobrir onde alguma funcionalidade do sistema baseia-se sobre os dados que são manipulados por essa funcionalidade. O sistema irá soletrar exatamente onde isso está acontecendo com base em sua arquitetura.

A implementação do Alexandria segue o princípio de interface explícita; invocar as operações reside no modelo de aplicativo e é mostrada no do Figura 2. Estou fazendo várias coisas interessantes aqui, então, let’s lidar com cada um deles em ordem.

De Adicionar um catálogo para a fila do usuário sobre o front-end, a Figura 2

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
    });
}

Primeiro, eu modifico o modelo de aplicativo diretamente para refletir imediatamente desejos do usuário. Posso fazer isso porque a adição de que um livro a fila do usuário é uma operação é garantida que nunca falha. Eu também remover da lista de recomendações, porque não faz sentido ter um item na fila do usuário também são exibidas na lista de recomendações.

Em seguida, enviar um lote de mensagem para o servidor back-end, informando que ele adicione o livro a fila do usuário e avise-me o que são recomendações e fila do usuário após essa alteração. Este é um conceito importante para entender.

A capacidade de compor a comandos e consultas dessa maneira significa que você Don seguir etapas especiais em comandos como AddBookToQueue para obter os dados alterados para o usuário. Em vez disso, o front-end pode pedi-la como parte do mesmo lote de mensagem e você pode usar a funcionalidade existente para obter esses dados.

Há duas razões que solicitam os dados do servidor back-end, embora eu faça as modificações na memória. Em primeiro lugar, o servidor back-end pode executar lógica adicional (como, por exemplo, localizando recomendações nova para este usuário), o que resultará em modificações que você Don conhecer no lado do front-end. Em segundo lugar, a resposta do servidor back-end irá atualizar o cache com o status atual.

Gerenciamento de estado de local desconectado

Talvez você tenha percebido um problema no 2 Figura com relação ao trabalho desconectado. Eu fizer uma modificação na memória, mas até que eu retornar uma resposta do servidor, os dados armazenados em cache não vai para refletir essas alterações. Se reiniciar o aplicativo, enquanto ainda é desconectado, o aplicativo exibirá informações expiradas. Uma vez que reinicia a comunicação com o servidor back-end, as mensagens seriam fluir para o back-end e o estado final deve resolver para que o usuário está esperando. Mas até esse momento, o aplicativo está exibindo informações de que o usuário já tenha sido alterado localmente.

Para aplicativos que esperam longos períodos de tempo de desconexão, Don confie apenas no cache de mensagem; em vez disso, implementar um modelo que tenha mantido após cada operação do usuário.

Para o aplicativo Alexandria, eu estendido as convenções de armazenamento em cache para expirar imediatamente quaisquer informações que faz parte de um lote de consultas e comando de mensagem, como o do Figura 2. Dessa forma, não tenho as informações atualizadas, mas também não mostro errado informações se o aplicativo for reiniciado antes de uma resposta do servidor back-end. Para fins do aplicativo Alexandria, que é o suficiente.

Processamento de back-end

Agora que você compreender como o processo funciona no lado do front-end de coisas, let’s olhar o código a partir do ponto de vista do servidor back-end. Você já estiver familiarizado com o tratamento de consultas, que mostrei no artigo anterior. A Figura 3 mostra o código para tratar um comando.

De Adicionar um catálogo para a fila do usuário, a Figura 3

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);
  }
}

O código real é bastante entediante. Posso carregar as entidades relevantes e, em seguida, chamar um método na entidade para realizar a tarefa real. No entanto, isso é mais importante que você imagina. Eu argumentaria é que trabalho de um arquiteto, certifique-se de que os desenvolvedores no projeto estão como bored possíveis. A maioria dos problemas de negócios são entediante e removendo as complexidades tecnológicas do sistema, você obtém um percentual muito maior de desenvolvedor de tempo gasto trabalhando em problemas de negócios entediante, em vez de problemas tecnológicos interessantes.

O que isso significa que no contexto de Alexandria? Em vez de se propagarem lógica de negócios em todos os consumidores de mensagem, que eu tenha centralizado como grande parte da lógica comercial possível nas entidades. Idealmente, consumir uma mensagem segue este padrão:

  • Carregar todos os dados necessários para processar a mensagem
  • Chamar um método único em uma entidade de domínio para executar a operação real

Esse processo garante que a lógica do domínio será permanecem no domínio. Para a qual essa lógica é — bem, o que deve os cenários em que você precisa manipular. Isso deve dar uma idéia sobre como posso lidar com a lógica de domínio em caso de 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
}

Você já viu um caso onde a lógica de front-end e a lógica de back-end são exatamente iguais. Agora let’s olhar um caso em que Don. Remover um livro da fila é muito simples na frente terminar (consulte do Figura 4). Ele é bastante direto. Remover o catálogo da fila localmente (o que ele é removido da interface do usuário), em seguida, envie um lote de mensagem para o back-end, pedindo para remover o catálogo da fila e atualizar a fila e as recomendações.

De remoção de um livro de fila, a Figura 4

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
    });
}

No back-end, consumindo a mensagem RemoveBookFromQueue segue o padrão mostrado no Figura 3 carregar as entidades e chamar o método 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
}

O comportamento é diferente entre o front-end e back-end. No back-end, adiciono o livro removido as recomendações, que Don faço no front-end. Qual seria o resultado da disparidade?

Bem, a resposta imediata seria remover o catálogo da fila, mas assim que as respostas do servidor back-end de alcançar o front-end, você verá o livro adicionado à lista de recomendações. Na prática, você provavelmente poderá notar a diferença, somente se o servidor back-end foi encerrado quando você remover um livro da fila.

Que está tudo muito bom, mas e quanto quando realmente precisar de confirmação do servidor back-end para concluir uma operação?

Operações complexas

Quando o usuário deseja adicionar, remover ou reordenar os itens na sua fila, é bastante óbvio que a operação pode falhar nunca, portanto, você pode permitir que o aplicativo a aceitar imediatamente a operação. Mas, para operações tais como edição de endereços ou alterar o cartão de crédito, você não pode aceitar apenas a operação até ter uma confirmação de sucesso do back-end.

Em Alexandria, isso é implementado como um processo de quatro estágios. Parece assustador, mas é realmente bem simples. A Figura 5 mostra os estágios possíveis.

A Figura 5 do quatro etapas possíveis para um comando que requer confirmação

A captura de tela da parte superior esquerda mostra o modo de exibição normal, os detalhes de assinatura. Isso é como Alexandria mostra alterações confirmadas. A captura de tela do canto inferior esquerdo mostra a tela de edição para os mesmos dados. Clicando em Salvar botão nessa tela resulta na captura de tela que mostra ao top–right; isso é como Alexandria mostra o alterações não confirmadas de .

Em outras palavras, eu aceitar a alteração (provisoriamente) até obter uma resposta do servidor indicando que a alteração foi aceita (o que move-nos de volta para a tela do canto superior esquerdo) ou rejeitadas, que move o processo para a captura de tela do canto inferior direito. A captura de tela mostra um erro do servidor e permite ao usuário corrigir o detalhe errado.

A implementação não é complexa, apesar do que talvez você ache. Eu vai iniciar no back-end e mover para fora. Figura 6 mostra o código de back-end necessário para lidar com isso e não é algo novo. Eu já fazendo a mesma coisa no decorrer deste artigo. A maioria das funcionalidades do comando condicional (e complexidade) reside em front-end.

Do tratamento de back-end de alteração de endereço de um usuário, a Figura 6

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
    });
  }
}

Uma coisa que seja diferente da que você já viu antes é que aqui o código de êxito/falha explícita para a operação, eu tenho enquanto antes simplesmente solicitou uma atualização de dados em uma consulta separada. A operação pode falhar e desejo saber não apenas se a operação for bem-sucedida ou não, mas por falhou.

Alexandria utiliza o Caliburn o framework para lidar com a maior parte do drudgery de gerenciar a interface do usuário. Caliburn (caliburn.codeplex.com ) é uma estrutura WPF/Silverlight depende muito de convenções para facilitar a criação de muitas das funcionalidades no modelo do aplicativo em vez de escrever código no código XAML por trás do aplicativo.

Como você verá observando o código de exemplo, quase tudo que na interface de usuário Alexandria é ligado através de XAML usando as convenções, dando-lhe claro e fácil de entender o XAML e um modelo de aplicativo que reflete diretamente a interface do usuário sem a necessidade de uma dependência direta sobre ele. Isso resulta em muito mais simples de código.

A Figura 7 deve dar uma idéia sobre como isso é implementado no modelo de modo de exibição SubscriptionDetails. Em essência, SubscriptionDetails contém duas cópias dos dados; um é mantido na propriedade editável e que é o que mostram todos os modos de exibição relacionados ao editar ou exibir as alterações não confirmadas. A segunda é mantida na propriedade Details, que é usada para manter as alterações confirmadas. Cada modo tem um modo de exibição diferente, e cada modo seleciona a partir do qual propriedade para exibir os dados.

De movimentação entre os modos de exibição em resposta à entrada de usuário, a Figura 7

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;
}

No XAML, eu com fio a ligação ViewMode para selecionar o modo de exibição apropriado para mostrar cada modo. Em outras palavras, a alternância de modo de edição irá resultar no modo de exibição Views.SubscriptionDetails.Editing.xaml sendo selecionado para mostrar a tela de edição para o objeto.

É a gravação e processos de confirmação será mais interessado, no entanto. Aqui está como posso lidar com salvamento:

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,
    }
  });
}

A única coisa que, na verdade, estou fazendo aqui é enviar uma mensagem e alternar o modo de exibição para um não-editável com um marcador informando que essas alterações ainda não foram aceitas. A Figura 8 mostra o código de confirmação ou rejeição. Considerando tudo, uma quantidade miniscule de código para implementar um recurso desse tipo e ele estabelece a base para implementar recursos semelhantes no futuro.

De consumo de resposta e o resultado de manipulação, a Figura 8

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;
}

Você também precisa considerar chamadas clássicas de solicitação/resposta, como, por exemplo, o catálogo de pesquisa. Porque a comunicação em tais chamadas é realizada por meio de mensagens unidirecionais, você precisará alterar a interface do usuário para indicar o processamento até que chegue a resposta do servidor back-end em segundo plano. Não entrarei em que o processo detalhadamente, mas o código para fazer isso existe no aplicativo de exemplo.

Fazendo check-out

No início desse projeto, iniciei informando as metas e os desafios de previsão de face na criação de um aplicativo desse tipo. Os principais desafios que objetivo endereços foram sincronização de dados, os fallacies de computação distribuída e manipulação de um cliente conectado ocasionalmente. Procurando novamente, pense que Alexandria faz um bom trabalho de reunião minhas metas e superar os desafios.

O aplicativo front-end é baseado no WPF e tornando pesado usar das convenções Caliburn para reduzir o código real para o modelo de aplicativo. O modelo está acoplado para os modos de exibição XAML e um pequeno conjunto de consumidores de mensagem de front-end que fazem chamadas para o modelo de aplicativo.

Abordei a lidar com unidirecional de mensagens, as mensagens na camada de infra-estrutura de armazenamento em cache e permissão para trabalhar desconectado, mesmo para as operações que exigem a aprovação de back-end antes que eles podem realmente ser considerados concluídos.

No back-end, criei um aplicativo de mensagens com base no barramento de serviços de Rhino e NHibernate. Abordei a gerenciar as vidas úteis de sessão e a transação e como você pode tirar proveito do cache de primeiro nível do NHibernate usando lotes de mensagens. Os consumidores de mensagem no back-end servem para consultas simples ou como os delegantes para o método apropriado em um objeto de domínio em que a maioria da lógica comercial, na verdade, reside.

Forçar o uso de comandos explícitos em vez de uma interface CRUD simples resulta em um código mais claro. Isso permite que você altere o código com facilidade, pois toda a arquitetura se concentra em definir claramente a função de cada parte do aplicativo e como deve ser criado. O resultado final é um produto muito estruturado, com linhas claras de responsabilidade.

É difícil tentar Aperte orientação para uma arquitetura de aplicativos distribuídos desenvolvida em algumas breves artigos, especialmente durante a tentativa de apresentar vários novos conceitos ao mesmo tempo. Ainda assim, acho que você descobrirá que a aplicação de práticas recomendadas descritas aqui resultará em aplicativos que são na verdade, mais fáceis de trabalhar com que as arquiteturas de baseados em RPC ou CRUD mais tradicionais.

Oren Eini (pseudônimo Ayende Rahien) é membro ativo de vários projetos de código-fonte aberto (entre eles, NHibernate e Castle) e fundador de muitos outros (como Rhino Mocks, NHibernate Query Analyzer e Rhino Commons). Eini também é responsável pelo criador de perfil do NHibernate (nhprof.com de ), um depurador visual para NHibernate. Você pode seguir o seu trabalho no ayende.com/Blogde .