Pontos de dados

Codificação para o design controlado por domínio: Dicas para desenvolvedores com foco em dados, Parte 3

Julie Lerman

Baixar o código de exemplo

Julie LermanEsta é a última parte da minha série que se concentra em ajudar os desenvolvedores com foco em dados a compreender alguns dos mais desafiadores conceitos de codificação usados com o DDD (Domain-Driven Design - Design Controlado por Domínio). Como um desenvolvedor do Microsoft .NET Framework que usa o Entity Framework (EF), e com uma longa história de desenvolvimento com foco em dados em primeiro lugar (e até mesmo com foco em banco de dados em primeiro lugar), eu enfrentei, argumentei e reclamei a minha maneira de compreender como mesclar minhas habilidades com algumas das técnicas de implementação de DDD. Mesmo que não esteja usando uma implementação de DDD completa (desde a interação com o cliente passando por todo o caminho até o código) em um projeto, eu ainda me beneficio amplamente de muitas das ferramentas de DDD.

Neste último artigo, discutirei dois importantes padrões técnicos de codificação de DDD e como eles se aplicam à ferramenta de ORM (Object-Relational Mapping - Mapeamento Relacional de Objetos) que utilizo, o EF. Em um artigo anterior, falei sobre relações um-para-um. Aqui, irei explorar as relações unidirecionais — preferencialmente com DDD — e como elas afetam o seu aplicativo. Essa escolha leva a uma decisão difícil: reconhecer quando pode ser melhor sem algumas das boas “mágicas” de relacionamento realizadas pelo EF. Também falarei um pouco sobre a importância de balancear tarefas entre uma raiz agregada e um repositório.

Criar relações unidirecionais a partir da raiz

Desde o momento em que comecei a criar modelos com o EF, as relações bidirecionais passaram a ser a norma, e eu fiz isso sem pensar muito. Faz sentido conseguir navegar em duas direções. Se você tem pedidos e clientes, é bom conseguir ver os pedidos de um cliente e, de acordo com o pedido, é conveniente acessar os dados do cliente. Sem pensar, eu também criei uma relação bidirecional entre os pedidos e seus itens de linha. Uma relação entre o pedido e os itens de linha faz sentido. Mas, se você parar para considerar tudo isso por um instante, os cenários em que você tem um item de linha e precisa voltar para o seu pedido são muito poucos. Um exemplo disso é você estar gerando relatórios sobre produtos e desejar analisar quais produtos são normalmente pedidos juntos ou realizar uma análise que envolva dados do cliente ou de remessa. Nesses casos, talvez seja necessário navegar de um produto para os itens de linha nos quais ele está contido e, em seguida, voltar para o pedido. Entretanto, consigo imaginar que isso ocorra apenas em um cenário de geração de relatórios, no qual não é provável que eu precise trabalhar com objetos com foco em DDD.

Se eu precisar apenas navegar do pedido para os itens de linha, qual é a maneira mais eficiente de descrever essa relação em meu modelo?

Como observei, o DDD prefere relações unidirecionais. Eric Evans aconselha que “é importante restringir as relações o máximo possível” e que “compreender o domínio pode revelar tendências direcionais naturais”. Gerenciar as complexidades das relações — especialmente quando você depende do Entity Framework para manter as associações — é definitivamente uma área que pode causar muita confusão. Eu já escrevi vários artigos para a coluna Pontos de Dados dedicados às associações no Entity Framework. Qualquer nível de complexidade que possa ser removido provavelmente será um benefício.

Observando o modelo simples de vendas que usei para esta série sobre DDD, ele apresenta uma tendência na direção de um pedido para seus itens de linha. Eu não consigo imaginar criar, excluir ou editar um item de linha sem começar pelo pedido.

Se voltar para as agregações de pedidos que criei anteriormente na série, você verá que o pedido controla os itens de linha. Por exemplo, você deve usar o método CreateLineItem da classe Order para adicionar um novo item de linha:

public void CreateLineItem(Product product, int quantity)
{
  var item = new LineItem
  {
    OrderQty = quantity,
    ProductId = product.ProductId,
    UnitPrice = product.ListPrice,
    UnitPriceDiscount = CustomerDiscount + PromoDiscount
  };
  LineItems.Add(item);
}

O tipo LineItem possui uma propriedade OrderId, mas nenhuma propriedade Order. Isso significa que é possível definir o valor de OrderId, mas não é possível navegar de uma instância LineItem para uma instância Order real.

Nesse caso, eu, nas palavras de Evans, “impus uma direção de passagem”. Na verdade, eu garanti que posso passar de Order para LineItem, mas não na outra direção.

Existem implicações para essa abordagem em relação ao modelo e, também, à camada de dados. Eu utilizo o Entity Framework como minha ferramenta de ORM, e ele abrange essa relação bem o suficiente, a partir da propriedade LineItems da classe Order. E, como aconteceu de eu seguir as convenções do EF, ele compreende que LineItem.OrderId é minha propriedade de chave estrangeira de volta para a classe Order. Se eu tivesse usado um nome diferente para OrderId, as coisas poderiam ser mais complicadas para o Entity Framework.

Mas, neste cenário, posso adicionar um novo LineItem a um pedido existente, conforme exibido a seguir:

order.CreateLineItem(aProductInstance, 2);
var repo = new SimpleOrderRepository();
repo.AddAndUpdateLineItemsForExistingOrder(order);
repo.Save();

Agora, a variável de pedido representa um gráfico com um pedido preexistente e um único LineItem novo. Esse pedido preexistente vem do banco de dados e já possui um valor em OrderId, mas o novo LineItem possui apenas o valor padrão para sua propriedade OrderId, que é 0.

Meu método de repositório obtém esse gráfico de pedido, o adiciona ao contexto do EF e, em seguida, aplica o estado apropriado, conforme exibido na Figura 1.

Figura 1 Aplicando o estado a um gráfico de pedido

public void AddAndUpdateLineItemsForExistingOrder(Order order)
{
_context.Orders.Add(order);
_context.Entry(order).State = EntityState.Unchanged;
foreach (var item in order.LineItems)
{
  // Existing items from database have an Id & are being modified, not added
  if (item.LineItemId > 0)
  {
    _context.Entry(item).State = EntityState.Modified;
  }
}
}

Caso não esteja familiarizado com o comportamento do EF, o método Add faz com que o contexto comece a acompanhar tudo no gráfico (o pedido e o único item de linha). Ao mesmo tempo, cada objeto no gráfico é sinalizado com o estado Adicionado. Mas, como esse método concentra-se no uso de um pedido preexistente, eu sei que Order não é novo e, por isso, o método fixa o estado da instância Order definindo-a como Não Alterado. Ele também verifica todos os LineItems preexistentes e define seus estados como Modificado, de forma que sejam atualizados no banco de dados, em vez de serem inseridos como novos. Em um aplicativo mais elaborado, eu usaria um padrão para saber de forma mais definitiva o estado de cada objeto, mas não quero que esta amostra fique sobrecarregada com detalhes adicionais. Você pode verificar uma versão anterior desse padrão no blog de Rowan Miller, em bit.ly/1cLoo14, e um exemplo atualizado em nosso livro escrito em conjunto “Programming Entity Framework: DbContext” (O’Reilly Media, 2012).

Como todas essas ações estão sendo feitas enquanto o contexto está monitorando os objetos, o Entity Framework também corrige “magicamente” o valor de OrderId em minha nova instância LineItem. Por isso, no momento que eu chamar Save, LineItem saberá que o valor de OrderId é 1.

Desistindo da mágica do gerenciamento de relações do EF — para atualizações

Essa sorte ocorre porque meu tipo de LineItem, por coincidência, segue a convenção do EF com o nome de chave estrangeira. Se o tiver nomeado com algo diferente de OrderId, como OrderFK, você talvez tenha que fazer algumas alterações ao seu tipo (por exemplo, introduzir a indesejada propriedade de navegação Order) e, em seguida, especificar os mapeamentos do EF. Isso não é desejável, pois você estaria adicionando complexidade simplesmente para satisfazer o ORM. Às vezes, isso pode ser necessário, mas, quando não é, eu prefiro evitar.

Seria mais simples apenas desistir de qualquer dependência da mágica de relação do EF e controlar a definição da chave estrangeira em seu código.

A primeira etapa é informar ao EF para ignorar essa relação; caso contrário, ele continuará a procurar por uma chave estrangeira.

Este é o código que usarei na substituição do método DbContext.OnModelBuilder, de forma que o EF não dê atenção a essa relação:

modelBuilder.Entity<Order>().Ignore(o => o.LineItems);

Agora, eu assumirei o controle da relação. Isso significa refatorar e, para isso, eu adiciono um construtor ao LineItem que exige um OrderId e outros valores, além de tornar o LineItem muito mais parecido com uma entidade de DDD e, assim, fico feliz. Também tenho de modificar o método CreateLineItem em Order para usar esse construtor, em vez de um inicializador de objeto.

A Figura 2 mostra uma versão atualizada do método de repositório.

Figura 2 O método de repositório

public void UpdateLineItemsForExistingOrder(Order order)
{
  foreach (var item in order.LineItems)
  {
    if (item.LineItemId > 0)
    {
      _context.Entry(item).State = EntityState.Modified;
    }
    else
    {
      _context.Entry(item).State = EntityState.Added;
      item.SetOrderIdentity(order.OrderId);
    }
  }
}

Observe que não estou mais adicionando o gráfico de pedido e, em seguida, fixando o estado do pedido como Não Alterado. Na verdade, como o EF não sabe da relação, se eu chamar context.Orders.Add(order), ele poderá adicionar a instância do pedido, mas não adicionará os itens de linha relacionados como fazia antes.

Em vez disso, estou fazendo a iteração por meio dos itens de linha do gráfico, e não apenas definindo o estado de itens de linha existentes como Modificado, mas definindo o estado de novos itens como Adicionado. A sintaxe DbContext.Entry que estou usando faz duas coisas. Antes de definir o estado, ela verifica se o contexto já sabe (ou “está acompanhando”) essa entidade específica. Em caso negativo, ela anexa a entidade internamente. Agora, ela consegue responder ao fato de que o código está definindo a propriedade de estado. Portanto, nessa única linha de código, eu estou anexando e definindo o estado de LineItem.

Meu código agora está de acordo com outra prescrição adequada para o uso do EF com DDD, que é: não depender do EF para gerenciar relações. O EF realiza muita mágica, um enorme bônus em muitos cenários. Eu me beneficio com isso há anos. Mas, para as agregações de DDD, você realmente deseja gerenciar essas relações no seu modelo, e não depender da camada de dados para executar as ações necessárias para você.

Como estou empacada, por enquanto, usando inteiros para minhas chaves (Order.OrderId, por exemplo) e dependendo do meu banco de dados para fornecer os valores dessas chaves, eu preciso fazer um trabalho extra no repositório para as novas agregações, como um novo pedido com itens de linha. Precisarei de um controle rígido da persistência, de forma que eu possa usar o antigo padrão de inserção de gráficos: inserir pedido, obter novo valor de OrderId gerado pelo banco de dados, aplicar isso aos novos itens de linha e salvá-los no banco de dados. Isso é necessário porque desfiz a relação que o EF normalmente usaria para executar essa mágica. Você pode ver no download de exemplo como eu implementei isso no repositório.

Estou pronta, após muitos anos, para parar de depender do banco de dados para criar meu identificador e começar a usar GUIDs para meus valores de chave, os quais posso gerar e atribuir em meu aplicativo. Isso me permite separar ainda mais meu domínio do banco de dados.

Mantendo a mágica do gerenciamento de relações do EF — para consultas

Renunciar ao meu modelo de relações do EF realmente me ajudou no cenário anterior para executar as atualizações. Entretanto, eu não quero perder todos os recursos de relação do EF. Carregar dados relacionados ao realizar consultas a partir do banco de dados é um recurso do qual não quero abrir mão. Independentemente de o carregamento ser rápido, lento ou explícito, eu adoro poder aproveitar a capacidade do EF de reunir dados relacionados sem ter de expressar e executar consultas adicionais.

É aí que entra uma visão estendida do conceito de separação de preocupações. Ao seguir os preceitos de DDD para design, não é incomum ter diferentes representações de classes semelhantes. Por exemplo, você pode fazer isso com uma classe Customer projetada para ser usada no contexto de gerenciamento de clientes, em oposição a uma classe Customer apenas para preencher uma lista de seleção que exige somente o identificador e o nome do cliente.

Também faz sentido ter definições de DbContext diferentes. Em cenários em que está recuperando dados, você talvez queira um contexto que saiba da relação entre Order e LineItems, de forma que seja possível carregar rapidamente um pedido juntamente com seus itens de linha a partir do banco de dados. Entretanto, quando estiver realizando atualizações, como fiz anteriormente, você talvez queira um contexto que ignore explicitamente essa relação, de forma que seja possível ter um controle mais granular de seu domínio.

Uma visão extrema disso para um determinado subconjunto de problemas complexos que você pode resolver com o software é um padrão chamado CQRS (Command Query Responsibility Segregation). O CQRS o orienta para pensar sobre a recuperação de dados (leituras) e o armazenamento de dados (gravações) como sistemas separados que podem exigir modelos e arquiteturas diferentes. Meu pequeno exemplo, que destaca os benefícios de ter as operações de recuperação de dados adotando uma compreensão diferente das relações das operações de armazenamento de dados, dá a você uma ideia do que o CQRS pode ajudá-lo a obter. Você pode obter mais informações sobre CQRS em nosso excelente recurso, o CQRS Journey, disponível em msdn.microsoft.com/library/jj554200.

O acesso aos dados ocorre no repositório, e não na raiz agregada

Agora, quero parar um pouco e analisar uma última questão que me atormentou quando comecei a me concentrar em relações unidirecionais. Isso não significa que não tenho mais dúvidas sobre o DDD, mas esse é o último tópico que abordarei nesta série. Essa questão sobre relações unidirecionais é comum para nós, pensadores do tipo “primeiro o banco de dados”: Onde ocorre, exatamente (com o DDD), o acesso aos dados?

Quando o EF foi lançado, a única maneira com que ele podia trabalhar com um banco de dados era fazendo a engenharia reversa de um banco de dados existente. Por isso, conforme indiquei anteriormente, me acostumei com todas as relações sendo bidirecionais. Se as tabelas Customers e Orders no banco de dados tivessem uma restrição de chave primária/chave estrangeira descrevendo uma relação um-para-muitos, eu podia ver essa relação no modelo. Customer tinha uma propriedade de navegação para um conjunto de pedidos. Order tinha uma propriedade de navegação para uma instância de Customer.

À medida que as coisas evoluíram para modelo e código primeiro, onde é possível descrever o modelo e gerar um banco de dados, eu continuei seguindo esse padrão, definindo as propriedades de navegação em ambas as extremidades de uma relação. O EF estava feliz, os mapeamentos estavam mais simples e a codificação mais natural.

Dessa maneira, com o DDD, quando me encontrei com uma raiz agregada de Order que estava ciente do CustomerId ou, até mesmo, de um tipo Customer completo, mas eu não conseguia navegar de Order para Customer, fiquei aborrecida. A primeira pergunta que fiz foi “e se eu quiser encontrar todos os pedidos de um cliente?”. Eu sempre presumi que precisaria fazer isso e estava acostumada a confiar no acesso à navegação nas duas direções.

Se a lógica começar com a minha raiz agregada do pedido, como eu poderia responder a essa pergunta? Inicialmente, eu também tinha o conceito errado de que você faz tudo por meio da raiz agregada, o que não ajudava.

A solução me fez quebrar a cabeça e me sentir um pouco tola. Compartilho minha tolice aqui para o caso de alguém ficar empacado dessa mesma maneira. Não é tarefa da raiz agregada nem de Order me ajudar a responder a essa pergunta. Entretanto, em um repositório com foco em pedidos, que é o que eu usei para executar minhas consultas e a persistência, não existem motivos para eu não poder ter um método para responder à minha pergunta:

public List<Order>GetOrdersForCustomer(Customer customer)
  {
    return _context.Orders.
      Where(o => o.CustomerId == customer.Id)
      .ToList();
  }

O método retorna uma lista de raízes agregadas de Order. É claro que, se estiver criando isso no mesmo escopo do DDD, eu só me preocuparia em colocar esse método em meu repositório se soubesse que ele seria necessário no contexto em particular, e não “só por precaução”. Provavelmente, eu precisaria dele em um aplicativo de geração de relatórios ou em algo semelhante, mas não necessariamente em um contexto projetado para a criação de pedidos de vendas.

Apenas o começo da minha jornada

Por ter aprendido sobre DDD nos últimos anos, os assuntos que abordei nesta série são aqueles em que encontrei mais dificuldade de compreender ou descobrir como implementar quando o Entity Framework fazia parte da minha camada de dados. Algumas das frustrações que encontrei estão relacionadas a anos de pensamento sobre o meu software de uma perspectiva de como as coisas poderiam funcionar em meu banco de dados. Deixar essa perspectiva de lado foi libertador, pois permite que eu me concentre no problema em questão, o problema do domínio para o qual estou criando o software. Ao mesmo tempo, preciso descobrir um balanço saudável, pois podem existir problemas na camada de dados que eu encontrarei no momento da adição à minha solução.

Enquanto me concentrei em como as coisas podem funcionar quando estou mapeando minhas classes diretamente de volta para o banco de dados com o Entity Framework, é importante considerar que pode haver outra camada (ou outras) entre a lógica de domínio e o banco de dados. Por exemplo, pode existir um serviço com o qual a sua lógica de domínio interaja. Nesse ponto, a camada de dados tem pouca (ou nenhuma) consequência no mapeamento de sua lógica de domínio; esse problema agora pertence ao serviço.

Existem várias maneiras de abordar as soluções de seu software. Mesmo quando não estou implementando uma abordagem de DDD completa (algo que exige um pouco de maestria), todo o meu processo continua a se beneficiar das lições e das técnicas que estou aprendendo com o DDD.

Julie Lerman é MVP da Microsoft, mentora e consultora do .NET, que reside nas colinas de Vermont. Você pode encontrá-la fazendo apresentações sobre acesso a dados e outros tópicos do Microsoft .NET Framework em grupos de usuários e conferências em todo o mundo. Seu blog está em thedatafarm.com/blog e é autora do livro “Programming Entity Framework” (2010), além das edições Code First (2011) e DbContext (2012), todos da O’Reilly Media. Siga Julie no Twitter em twitter.com/julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Stephen Bohlen (Microsoft)