Este artigo foi traduzido por máquina.

Padrões em prática

Padrões de persistência

Jeremy Miller

Conteúdo

Mapeamento de objetos para bancos de dados
Registro ativo
Mapeador de pontos de dados
Usando um repositório
Mapa de identidade
Ao carregar lenta E ansioso
Virtual padrão de proxy
Fazer a próxima etapa

Acesso a dados é um assunto popular entre os desenvolvedores. Sem dúvida você ouviu muita opiniões em tecnologias de acesso a dados específicos e estruturas de persistência, mas qual é a melhor maneira para consumir essas ferramentas em seu projeto? Critérios que você deve usar para selecionar a ferramenta certa para o seu projeto? O que você precisa fazer saber conceitualmente sobre essas ferramentas antes de usar? Se você tem muito tempo em suas mãos e quer escrever sua própria ferramenta persistência — o que você precisa saber?

Unsurprisingly, a resposta para todas essas perguntas é examinar os padrões de design subjacente de persistência.

Modelos de domínio

Quando você pensar em como você vai a estrutura e representar a lógica comercial em seu sistema, você tem duas opções principais. Neste artigo, amplamente presumo que você escolheu a abordagem de modelo de domínio para organizar a lógica comercial em objetos de entidade.

A partir descrição formal, um modelo de domínio é um modelo de objeto do domínio que incorpora o comportamento e dados.

Por exemplo, o projeto atual envolve gerenciamento de relacionamento com o cliente. (CRM) Temos objetos de entidade para caso o usuário e solução que contêm os dados e implementar regras comerciais que envolvem dados. Um modelo de domínio pode variar de um modelo anêmica que é simplesmente um conjunto de estruturas de dados para um modelo muito sofisticado que jealously guarda os dados brutos por trás uma interface estreito (fundamental Domain-Driven desenvolvimento). Onde o modelo de domínio ficar nesse intervalo é amplamente uma questão de como complicada a lógica comercial no sistema realmente é e como predominantes relatórios ou entrada de dados estão em seus requisitos de sistema.

Uma discussão adequada do modelo de domínio padrão está além do escopo deste artigo. É altamente seria recomendável ler o capítulo 2 do catálogo de padrões de aplicativo arquitetura do Martin Fowler para uma ótima discussão sobre os padrões de design principais para a organização lógica comercial.

Antes de começar, vamos rever as duas maneiras principais de percebem a função do código de acesso banco de dados e dados no seu sistema:

  • O banco de dados é o keystone do aplicativo e um ativo comercial. O código de acesso a dados e até mesmo o código do aplicativo ou serviço são simplesmente os mecanismos para conectar o banco de dados com o mundo exterior.
  • Os objetos comerciais na camada intermediária e a camada de interface ou o serviço de usuário são o aplicativo e o banco de dados é um meio confiável persistir o estado dos objetos comerciais entre as sessões.

Pessoalmente, sinto como o primeiro, ponto de vista datacêntricos é bem compreendido na comunidade do .NET, portanto, gostaria de enfocar o ponto de vista da segundo. No ponto de vista segundo, normalmente você está trabalhando com objetos de entidade na camada intermediária. Uma forma ou de outra, você provavelmente está fazendo mapeamento de objeto/relacional (O/RM) para mapear dados das entidades comerciais para as tabelas do banco de dados e vice-versa. Você pode ser fazer isso manualmente, mas provavelmente com ferramentas de algum tipo. Essas ferramentas são excelentes e potencialmente podem economizar muito tempo de desenvolvimento, mas existem algumas questões que você deve estar ciente de e é sempre bom compreender como uma ferramenta funciona sob as capas. Felizmente, estudar os padrões neste artigo ajudará nas duas contas.

EU sou um proponent grande de ágil recomendadas como Test Driven Development, comportamento Driven Development, Lean programação e design contínua. Digo isso apenas seja desmarque que eu tenho uma inclinação muito específica em relação a padrões que vai ser abordar neste artigo, e provavelmente mostrará.

Mapeamento de objetos para bancos de dados

Decidi modelo de objetos do meu sistema na camada intermediária com entidade que têm identidade, dados e o comportamento relacionado. Minha lógica de negócios serão ser implementada nesses objetos de entidade ou em serviços de domínio que usam esses objetos de entidade. Ótimo, mas como fazer você perfeitamente dados de mover e para trás entre o banco de dados e os objetos de entidade?

No caso de um banco de dados relacional, você precisará mover os campos e propriedades de nossos objetos para tabelas e campos no banco de dados. Você pode escrever este código completamente mão, escrever out separadas instruções INSERT, UPDATE, SELECT e DELETE do SQL, mas será rapidamente percebe que você está repetição mesmo no código bastante. Ou seja, você está especificando repetidamente que os dados em uma propriedade do objeto ou o campo devem ser armazenados em uma coluna específica em uma tabela de banco de dados.

Isso é onde um mapeador de objeto/relacional (O/RM) etapas em. Quando você usa um o/Gerenciador de recursos, basta criar um mapeamento das propriedades de objeto para a tabela de banco de dados e deixar que a ferramenta o/Gerenciador de recursos usar esses metadados para descobrir quais devem ser as instruções SQL e como mover dados do objeto para as instruções SQL.

Vamos obter concreto e examinar um exemplo bem básico. Meu projeto atual tem uma classe de endereço que tem esta aparência:

public class Address 
{
  public long Id { get; set; }
  public string Address1 { get; set; }
  public string Address2 { get; set; }
  public string City { get; set; }
  public string StateOrProvince { get; set; }
  public string Country { get; set; }
  public string PostalCode { get; set; }
  public string TimeZone { get; set; }
}

Quando eu configura o mapeamento para a classe de endereço, precisa especificar qual tabela os mapas de classe de endereço para, como os objetos de endereço será identificado (a chave primária) e as propriedades que mapeiam para quais tabelas de banco de dados.

Neste exemplo, ESTOU usando a ferramenta NHibernate Fluent para mapeamento de endereço.

Intencionalmente ESTOU fazendo o mapeamento de maneira longhand para mostrar todos os detalhes. (Em uso real, utilizam convenção sobre configuração Para eliminar muita o repetitiveness.) Eis o código:

public class AddressMap : ClassMap<Address> 
{
  public AddressMap() 
  {
    WithTable("Address");
    UseIdentityForKey(x => x.Id, "id");

    Map(x => x.Address1).TheColumnNameIs("address1");
    Map(x => x.Address2).TheColumnNameIs("address2");
    Map(x => x.City).TheColumnNameIs("city");
    Map(x => x.StateOrProvince).TheColumnNameIs("province_code");
    Map(x => x.Country).TheColumnNameIs("country_name");
    Map(x => x.PostalCode).TheColumnNameIs("postal_code");
  }
}

Agora que feitas um mapeamento do modelo de objeto do modelo de banco de dados, algo deve realmente executar o mapeamento e você tem aproximadamente duas opções: registro Active ou mapeador de pontos de dados.

Registro ativo

Ao escolher uma estratégia de persistência, a primeira decisão que você precisa fazer é onde colocar a responsabilidade de realizar o mapeamento. Você tem duas opções muito diferentes: você pode ou fazer cada própria classe de entidade responsável pelo mapeamento, ou você pode usar uma classe completamente separada para fazer o mapeamento para o banco de dados.

A primeira opção é conhecida como o padrão de registro Active: um objeto que envolve uma linha em uma tabela de banco de dados ou modo de exibição, encapsula o acesso ao banco de dados e adiciona lógica de domínio que os dados. Uma abordagem de registro Active coloca os métodos de persistência diretamente para o objeto de entidade. Nesse caso, a classe de endereço provavelmente seria necessário métodos como salvar, Update e Delete, bem como um método de carga estático que consulta o banco de dados para um objeto de endereços.

O uso típico de padrão de registro Active é essencialmente um invólucro de classe com rigidez de tipos em torno de uma única linha em uma tabela de banco de dados. Assim, as classes de Registro Active são geralmente um espelho exato da estrutura de banco de dados (isso varia entre ferramentas). Muitas implementações de registro Active gerará os objetos de entidade diretamente da estrutura de banco de dados.

A implementação de registro Active mais famoso é a ferramenta ActiveRecord que vem como parte do the Ruby na estrutura de desenvolvimento da Web cadeiras (que é útil na programação do .NET com IronRuby; consulte" Introdução ao IronRuby E RSpec, parte 1"). Se eu estivesse usando ActiveRecord, primeiro deve criar uma tabela chamada endereços no banco de dados. Em seguida, se todos me importo com está tendo acesso aos dados de endereço e métodos para localizar, salvando e inserir dados de endereço, a classe de endereço inteira aparência exatamente essa classe rubi:

class Address < ActiveRecord::Base
end

A implementação de rubi está usando metaprogramming para criar campos e métodos que correspondem a tabela de banco de dados nomeada endereços (formulário pluralized do nome da classe endereço) em tempo de execução da consulta dinamicamente.

Com poucas exceções, implementações de .NET do padrão de registro Active geralmente funcionam, usando a geração de código para criar classes .NET que mapeiam diretamente para tabelas de banco de dados. A vantagem dessa abordagem é que é muito fácil manter o modelo de banco de dados e o objeto sincronizado.

Inclinar programação

Programação Lean ensina a eliminar esforço desperdiçado em projetos de desenvolvimento por favorecendo design de "recepção" sobre design de "envio". Isso significa questões de infra-estrutura como persistência só deve ser criada e criada para satisfazer as necessidades de requisitos de negócios (retirados sob demanda) em vez de criar o código de camada de acesso a dados que você acha que o aplicativo terá mais tarde (enviado).

Na minha experiência, isso significa que você deve desenvolver o sistema de forma incremental desenvolvendo verticalmente — criando um recurso por vez em vez de criar a camada horizontal de um sistema por vez. Por trabalhar dessa forma você pode ter certeza de que desenvolvimento de infra-estrutura é não mais do que o que é absolutamente necessário, amarrando todo código de infra-estrutura para um recurso que está sendo criado no sistema. Quando você trabalha horizontalmente com a criação da camada de acesso de dados ou o banco de dados antes de escrever a interface do usuário, serviço ou camada de lógica comercial, você riscos o seguinte:

  • Não obtendo comentários adequado antes dos participantes do projeto porque não há nenhuma funcionalidade de trabalho para demonstrar
  • Escrever código de infra-estrutura desnecessárias para os recursos que não pode obter, na verdade, criado
  • Criar o código de acesso de dados errado porque você não compreender a necessidade de negócios no início ou os requisitos de alterar antes das outras camadas são criadas

Design de recepção também significa que a escolha da estratégia de acesso de dados deve ser determinada pelas necessidades do aplicativo. Tem uma opção, escolheria um o/Gerenciador de recursos para um sistema em que foi modelagem a lógica comercial em um modelo de domínio. Para um aplicativo de geração de relatórios, eu poderia ignorar persistência ferramentas totalmente em favor de apenas usando SQL e conjuntos de dados. Para um sistema que é principalmente entrada de dados, pode escolher uma ferramenta de registro Active. A questão é que o acesso a dados e persistência pelas necessidades de seus consumidores.

Mapeador de pontos de dados

BOM como o registro Active padrão pode ser, geralmente é útil para ter uma estrutura de objeto que varia de modelo de banco de dados. Convém mapear várias classes para a mesma tabela de banco de dados. Você pode ter um esquema de banco de dados herdados que não se ajustar a forma dos objetos desejados seria para expressar em alguma lógica de negócios (uma ocorrência comum para mim em Meus últimos projetos vários).

Isso é onde eu escolha o padrão de mapeador de pontos de dados: uma camada de objetos do mapeador de pontos que mover dados entre objetos e um banco de dados, mantendo-los independente de uns aos outros e para o mapeador de pontos de classes próprias. O mapeador de pontos de dados padrão remove a maioria da responsabilidade de persistência dos objetos entidade em favor de classes externos para as entidades. Com um padrão de mapeador de pontos de dados, você pode acessar, consulta e salva objetos de entidade com algum tipo de repositório (discutido posteriormente neste artigo).

Como escolher entre os dois? Meu diferença de pessoal é muito forte para a solução de mapeador de pontos de dados. Essa exceção, registro Active é ideal para sistemas com a lógica de domínio mais simples, CRUD-muitos aplicativos (criar, ler, atualizar e excluir, isto é) e situações em que o modelo de domínio não precisa divergem muito da estrutura de banco de dados. Registro ativo talvez seja mais confortável para muitas equipes de desenvolvimento do .NET porque isso implica uma forma de datacêntricos de trabalho que é mais comum para as equipes do .NET e, francamente, muito melhor suportado pelo .NET propriamente dito. Eu poderia caracterizam a maioria das ferramentas persistência no espaço de .NET como ferramentas de Registro Active.

Por outro lado, mapeador de pontos de dados é mais apropriado para sistemas com lógica complexa de domínio onde a forma do modelo de domínio será divergem consideravelmente do modelo de banco de dados. Mapeador de pontos de dados também dissocia suas classes de modelo de domínio do armazenamento de persistência. Que pode ser importante para casos em que você precise reutilizar o modelo de domínio com mecanismos de banco de dados diferente, esquemas e mecanismos de armazenamento diferentes completamente.

Mais importante para mim como um profissional ágeis, a abordagem do mapeador de pontos de dados permite criar o modelo de objeto independentemente do banco de dados com Test Driven Development forma mais eficiente do que um registro de Active foi de solução. Isso é importante para equipes que deseja uma abordagem incremental para criar porque ele permite que uma equipe para examinar o design no modelo de objeto do primeiro onde refatoração ferramentas e técnicas de teste de unidade são geralmente mais eficazes e criar o modelo de banco de dados posteriormente quando o modelo de objeto é estável. O padrão de mapeador de pontos de dados também é mais aplicável à abordagem de arquitetura domínio controlada por design que está ganhando popularidade na comunidade do .NET.

Usando um repositório

Quando eu foi aumentando backup como um desenvolvedor nos dias Windows DNA, eu vivia em mortal medo de esquecer de fechar um objeto de conexão ADO em meu código. Em meu projeto de empresa de grande primeiro, eu codificado diretamente no ADO. Sempre que eu necessário para solicitar ou salvar dados no banco de dados, eu tive que faça o seguinte:

  1. Localizar a seqüência de conexão de algum tipo de configuração
  2. Abrir uma nova conexão para o banco de dados
  3. Criar um objeto de comando ou um objeto recordset
  4. Executar a instrução SQL ou procedimento armazenado
  5. Fechar a conexão para liberar os recursos de banco de dados
  6. E AH Sim, coloque algum erro adequado tratamento ao redor o código de acesso a dados

O primeiro problema era a quantidade absurd de código repetitivo que eu estava escrevendo. O segundo problema era que eu tive que escrever o código corretamente cada vez, porque esquecer fechar uma única conexão poderia e Infelizmente, unidade um sistema de missão crítica off-line sob uma carga pesada. Ninguém quiser ser o rapaz cujo código recolhido fora do banco de dados ruim conexão higienização, mas ele ainda aconteceu com muita freqüência.

No meu próximo projeto, meu colega de trabalho e tem mais inteligentes. Implementamos uma única classe que poderia fazer todos os programa de instalação, desmontagem e código do objeto ADO Connection de tratamento de erros. Qualquer outra classe no sistema pode interagir com o banco de dados usando essa classe central para chamar procedimentos armazenados. Escrevemos muito menos código e, melhor ainda, não foram plagued por problemas de aparecimento de esquecer fechar conexões de banco de dados porque tivemos que obter esse código de gerenciamento de conexão direito apenas uma vez.

Que minha equipe fez era criar uma implementação crua do repositório padrão que intermediário entre o domínio e dados de mapeamento camadas usando uma interface de coleção-como para acessar objetos de domínio.

Basicamente, o padrão de repositório apenas significa colocar uma fachada sobre o sistema de persistência para que você pode proteger o restante do código do aplicativo tenha de saber como funciona a persistência. Meu projeto atual está usando um repositório com uma interface pública que pareça que na Figura 1 .

A Figura 1 repositório interface

public interface IRepository 
{
  // Find an entity by its primary key
  // We assume and enforce that every Entity
  // is identified by an "Id" property of 
  // type long
  T Find<T>(long id) where T : Entity;

  // Query for a specific type of Entity
  // with Linq expressions.  More on this later
  IQueryable<T> Query<T>();
  IQueryable<T> Query<T>(Expression<Func<T, bool>> where);

  // Basic operations on an Entity
  void Delete(object target);
  void Save(object target);
  void Insert(object target);

  T[] GetAll<T>();
}

Quando alguma classe no sistema precisa acessar um objeto de entidade, ele pode simplesmente usar IRepository para buscar dessa entidade pela sua identificação, ou consulta para uma lista de objetos de entidade com uma expressão do LINQ.

Ao usar a classe concreta de IRepository, que usa a biblioteca NHibernate para o/Gerenciador de recursos, estou recebendo uma seqüência de conexão da memória, carregar as definições de mapeamento de um assembly, criando o SessionFactory NHibernate (uma vez e apenas uma vez porque é um impacto no desempenho grande) e quebra a interface de ISession nível baixo de NHibernate. Com alguns ajuda da classe repositório, NHibernate gerencia problemas de ciclo de vida de conexão de banco de dados.

Uau. Isso é muita coisa acontecendo nos bastidores. É uma coisa boa que eu tenha swept todas a interação direta com NHibernate por trás da interface IRepository. Eu não tenho saber todos os que bootstrapping NHibernate coisas apenas para carregar, salvar e consultar objetos. Melhor ainda, desde que todas as classes depende a interface de IRepository abstrata para acesso a dados e persistência, pode deslizar em uma implementação InMemoryRepository de IRepository que usa o LINQ to Objects internamente para stub o banco de dados durante o teste.

Mapa de identidade

Vejamos um cenário comum. Dentro de uma única transação lógica em algum tipo de sistema de entrega, você tem duas classes completamente diferentes que funcionam independentemente, mas ambas as classes precisará recuperar a mesma entidade cliente durante a transação. O ideal é que você deseja apenas um único objeto cliente dentro de uma única transação para cada cliente lógica para que cada objeto está funcionando de dados consistente.

Em ferramentas de persistência, evitando referências duplicadas lógicas é o trabalho do mapa de identidade padrão. Conforme indicado pelo Fowler Martin, um mapa de identidade garante que cada objeto é carregado apenas uma vez mantendo cada objeto carregado em um mapa e procura objetos usando o mapa quando se referem a elas.

Para a classe de cliente, você pode criar uma implementação de naïve do mapa de identidade como da Figura 2 . Eu propositadamente estou deixando out bloqueio aqui apenas para simplificar o código de segmento. Uma implementação real exigiria medidas de segurança do thread adequada.

A Figura 2 classe de cliente do mapa de identidade

public class CustomerRepository 
{
  public IDictionary<long, Customer> _customers = 
    new Dictionary<long, Customer>();

  public Customer FindCustomer(long id) 
  {
    if (_customers.ContainsKey(id)) 
  {
      return _customers[id];
    }

    var customer = findFromDatabase(id);
    _customers.Add(id, customer);

    return customer;
  }

  private Customer findFromDatabase(long id) 
  {
    throw new System.NotImplementedException();
  }
}

Neste exemplo, um objeto Customer é identificado por sua identificação. Quando você solicita uma instância do cliente pela identificação, o CustomerRepository primeiro verifica um dicionário interno para ver se ele tem que cliente específico. Se afirmativo, retorna o objeto de cliente existente. Caso contrário, CustomerRepository será buscar os dados do banco de dados, criar um novo cliente, armazenar esse objeto de cliente em seu dicionário para solicitações posteriores e retornar o novo objeto de cliente.

Felizmente, você geralmente não gravar este código mão porque qualquer ferramenta de persistência maduro deve incluir esse recurso. É necessário estar ciente de que isso está acontecendo nos bastidores e escopo os objetos de suporte de persistência adequadamente. Muitas equipes usará o recurso de gerenciamento de ciclo de vida de uma ferramenta de inversão de controle (StructureMap, Windsor, Ninject e outros) para garantir que todas as classes em uma única solicitação HTTP ou thread estão usando o mesmo subjacente mapa de identidade. O padrão de unidade de trabalho é outra maneira de gerenciar um único mapa de identidade em várias classes na mesma transação lógica.

Apenas para ilustrar esse padrão ainda mais, Figura 3 mostra um exemplo de como funciona um mapa de identidade. O código é escrito em relação a arquitetura do meu projeto atual. As instâncias da interface de IRepository mostrada no código abaixo quebra um único NHibernate ISession, que por sua vez implementa o padrão de mapa de identidade. Quando eu executar esse teste a saída é esta:

1 passed, 0 failed, 0 skipped, took 5.86 seconds.

A Figura 3 usando um mapa de identidade

[Test]
public void try_out_the_identity_map() 
{
  // All I'm doing here is getting a fully formed "Repository"
  // from an IoC container and letting an IoC tool bootstrap 
  // NHibernate offstage.
  IRepository repository = ObjectFactory.GetInstance<IRepository>();

  // Find the Address object where Id == 1
  var address1 = repository.Find<Address>(1);

  // Find the Address object where Id == 1 from the same Repository
  var address2 = repository.Find<Address>(1);

  // Requesting the same identified Address object (Id == 1) inside the 
  // same Repository / Identity Map should return the exact same
  // object
  address1.ShouldBeTheSameAs(address2);

  // Now, let's create a completely new Repository that has a 
  // totally different Identity Map
  IRepository secondRepository = ObjectFactory.GetInstance<IRepository>();

  // Nothing up my sleeve...
  repository.ShouldNotBeTheSameAs(secondRepository);

  var addressFromSecondRepository = secondRepository.Find<Address>(1);

  // and this is a completely different Address object, even though
  // it's loaded from the same database with the same Id
  addressFromSecondRepository.ShouldNotBeTheSameAs(address1);
}

Ao carregar lenta E ansioso

Uma das melhores coisas sobre como usar uma ferramenta de persistência é a capacidade de carregar um objeto raiz (fatura, talvez), em seguida, navegar diretamente para seus filhos (InvoiceLineItem) e objetos relacionados apenas usando propriedades da classe pai. No entanto, mais cedo ou mais tarde você vai ter ao se importar com o desempenho de seu aplicativo. Buscar um gráfico de objeto inteiro quando você pode precisar somente de nível superior na maioria das vezes de objeto ou pode não partes do objeto gráfico não é eficiente.

Tudo bem. Nesse caso, você pode usar o padrão de carregamento lento no qual você adiar inicializando o objeto até imediatamente antes que ele é necessário.

Vamos colocar isso em termos mais concretos. Digamos que que você tenha uma classe chamada cliente que faz referência a um objeto de endereços:

public class Customer : DomainEntity 
{
  // The "virtual" modifier is important.  Without it,
  // Lazy Loading can't work
  public virtual Address HomeAddress { get; set; }
}

Na maioria usar casos envolvendo o objeto cliente, o código nunca precisa a propriedade Customer.HomeAddress. Nesse caso, você pode configurar o mapeamento do banco de dados para tornar a propriedade de Customer.HomeAddress lenta carregada como nesse mapeamento NHibernate Fluent:

public class CustomerMap : DomainMap<Customer> 
{
  public CustomerMap() 
  {
    // "References" sets up a Many to One
    // relationship from Customer to Address
    References(x => x.HomeAddress)
      .LazyLoad() // This marks the property as "Lazy Loaded"
      .Cascade.All();
  }
}

Com lento carregando ativado, o objeto Customer é obtido sem os dados de endereços. No entanto, assim que qualquer chamador tenta acessar a propriedade Customer.HomeAddress pela primeira vez, os dados serão transparente carregados.

Observe o modificador virtual na propriedade Customer.HomeAddress. Nem toda ferramenta persistência faz isso, mas NHibernate implementa lentas propriedades carregadas, criando uma subclasse dinâmica de cliente que substitui a propriedade HomeAddress para tornar lenta carregado. A propriedade HomeAddress precisa a ser marcado como virtual para permitir que uma subclasse substituir a propriedade.

É claro, há outras vezes, quando você solicita um objeto e você sabe que provavelmente será necessário seus filhos ao mesmo tempo. Nesse caso, você provavelmente será optar por carregar ansioso e tem os dados de filhos carregados ao mesmo tempo como pai. Muitas ferramentas de persistência terá algum tipo de capacidade para otimizar o carregamento ansioso cenários buscar uma hierarquia de dados em uma viagem de ida e volta único banco de dados. Se você precisar os dados Customer.HomeAddress na maioria das vezes que você usar um objeto cliente, em seguida, você poderia ser melhor fazer carregar ansioso para obter os dados do cliente e o endereço ao mesmo tempo.

Neste ponto, deve repetir o maxim antigo que a única maneira confiável ajustar um aplicativo para desempenho deverá usar uma empírica medição de desempenho com um gerador de perfil.

Virtual padrão de proxy

Ao carregar lenta geralmente é implementado usando um objeto proxy virtual que pareça apenas o objeto real deve ser carregado posteriormente. Digamos que o modelo de domínio inclui uma classe chamada CustomerRepresentative que faz referência a uma lista de objetos de cliente. Parte dessa classe é mostrado na Figura 4 .

A Figura 4 CustomerRepresentative

public class CustomerRepresentative 
{
  // I can create a CustomerRepresentative directly
  // and use it with a normal List
  public CustomerRepresentative() 
  {
    Customers = new List<Customer>();
  }

  // Or I can pass an IList into it.
  public CustomerRepresentative(IList<Customer> customers) 
  {
    Customers = customers;
  }

  // It's not best practice to expose a collection
  // like I'm doing here, but it makes the sample
  // code simpler ;-)
  public IList<Customer> Customers { get; set; }
}

Existem muitas vezes quando o sistema usa uma instância de CustomerRepresentative sem precisar a lista de clientes. Nesse caso, você pode simplesmente construir um CustomerRepresentative com um objeto proxy virtual que é semelhante um IList <customer> objeto e usar essa classe de proxy virtual sem fazer qualquer alteração CustomerRepresentative qualquer. Essa classe de proxy virtual poderia ser algo assim Figura 5 . Um objeto de CustomerRepresentative, em seguida, pode ser criado com carregamento lento conforme mostrado na Figura 6 .

A Figura 5 virtual proxy CustomerRepresentative

public class VirtualProxyList<T> : IList<T> 
{
  private readonly Func<IList<T>> _fetcher;
  private IList<T> _innerList;
  private readonly object _locker = new object();

  // One way or another, VirtualProxyList needs to 
  // find the real list.  Let's just cheat and say
  // that something else will pass it a closure
  // that can find the real List
  public VirtualProxyList(Func<IList<T>> fetcher) 
  {
    _fetcher = fetcher;
  }

  // The first call to 
  private IList<T> inner 
  {
    get 
    {
      if (_innerList == null) 
      {
        lock (_locker) 
        {
          if (_innerList == null) 
          {
            _innerList = _fetcher();
          }
        }
      }

      return _innerList;
    }
  }


  IEnumerator IEnumerable.GetEnumerator() 
  {
    return inner.GetEnumerator();
  }

  public IEnumerator<T> GetEnumerator() 
  {
    return inner.GetEnumerator();
  }

  public void Add(T item) 
  {
    inner.Add(item);
  }

  // and the rest of the IList<T> implementation

} 

A Figura 6 lento carregando CustomerRepresentative

public class CustomerRepresentativeRepository 
{
  private readonly ICustomerRepository _customers;

  public CustomerRepresentativeRepository(
      ICustomerRepository customers) 
  {
    _customers = customers;
  }

  // This method will "find" a CustomerRepresentative, and
  // set up the Virtual Proxy for the Customers
  public CustomerRepresentative Find(long id) 
  {
    var representative = findRepresentative(id);
    representative.Customers = 
      new VirtualProxyList<Customer>(() => 
      _customers.GetCustomersForRepresentative(id));

    return representative;
  }
}

Como a maioria dos padrões neste artigo, o proxy virtual não é algo que você provavelmente escrever manualmente, mas esteja ciente de que ele é não existe no plano de fundo da sua ferramenta de persistência.

Fazer a próxima etapa

Quando eu começou a programação com tecnologias Windows DNA, provavelmente passei em metade do meu tempo trabalhando com código ADO não processado. Hoje, codificação de persistência e infra-estrutura é uma porcentagem muito pequena de tempo de minha equipe. Portanto, o que mudou com o passar dos anos? Usamos as ferramentas de persistência e esses padrões de design para eliminar muito a codificação repetitiva que são usadas para fazer.

A maioria desses padrões foram tirada da livro do Martin Fowler, padrões de aplicativo arquitetura. Eu recomendo ler este livro se você tiver nada para fazer com a criação de aplicativos empresariais. Devido às limitações de tamanho, não consegui abordam outros padrões importantes como unidade de trabalho, especificações e persistência ignorância. Além disso, há uma série de maneiras de usar as repositório padrão e design considerações (um único genérico repositório versus específicos, "restringir" classes do repositório, se um repositório mesmo deve expor "salvar" métodos, etc..) Eu poderia instigam você pesquisar esses tópicos também, e pode escrever um artigo de acompanhamento para continuar essa discussão.

Finalmente, gostaria dizer que o ecossistema do .NET é mais que apenas a estrutura de entidades e LINQ para SQL. Estou satisfeito com NHibernate como um mapeador de dados que sabe relativamente pouco sobre persistência. SubSonic é uma implementação comum do registro Active para programação do .NET. iBatis.Net é fantástica para bancos de dados existentes ou ocasiões quando deseja total controle sobre a criação as instruções SQL. LLBLGen Pro é uma ferramenta muito madura com capacidades de consulta exclusivas. Muitos das outras ferramentas também têm a capacidade de usar consultas LINQ.

Envie suas dúvidas e comentários para mmpatt@microsoft.com.

Jeremy Miller, um MVP da Microsoft para o C#, também é autor da (de StructureMap abertostructuremap.SourceForge. NET) ferramenta de injeção de dependência com .NET e a próxima (StoryTellerstoryteller.tigris.org) ferramenta para testar FIT supercharged no. NET. Visite seu blog "O desenvolvedor de árvore de sombra" parte do site CodeBetter.