Pontos de dados

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

Julie Lerman

Baixar o código de exemplo

Julie LermanA coluna deste mês continuará a celebrar o 10º aniversário do livro de Eric Evans, “Domain-Driven Design: Tackling Complexity in the Heart of Software” (Addison-Wesley Professional, 2003). Irei compartilhar mais dicas para desenvolvedores com foco em dados em primeiro lugar que estejam interessados em se beneficiar de alguns dos padrões de codificação do DDD (Design controlado por domínio). A coluna do mês anterior destacou:

  • colocar as preocupações com persistência de lado durante a modelagem do domínio
  • Expor métodos para orientar suas entidades e agregações, mas não seus setters de propriedade
  • Reconhecer que alguns subsistemas são perfeitos para interação de CRUD (criar, ler, atualizar e excluir) e não requerem a modelagem do domínio
  • O benefício de não tentar compartilhar dados ou tipos entre contextos limitados

Nesta coluna você aprenderá o que significam os termos modelos de domínio "anêmico" e "avançado", bem como o conceito de objetos de valor. Os objetos de valor parecem ser um tópico que se encaixa em um de dois grupos para os desenvolvedores. São tão óbvios que alguns leitores nem conseguem imaginar porque estão sendo discutidos ou tão confusos que outros nunca conseguiram entendê-los. Também incentivarei você a considerar o uso de objetos de valor em vez de objetos relacionados em determinados cenários.

O termo condescendente: modelos de domínio anêmicos

Há dois termos que você ouvirá com frequência em relação a como as classes são definidas no DDD, modelo de domínio anêmico e modelo de domínio avançado. No DDD, modelo de domínio se refere a uma classe. Um modelo de domínio avançado é um modelo que se alinha com a abordagem do DDD, uma classe (ou tipo) que é definida com comportamentos, não só com getters e setters. Por outro lado, um modelo de domínio anêmico consiste simplesmente em getters e setters (e talvez alguns métodos simples), que é bom para muitos cenários, mas que não obtém nenhum benefício no DDD.

Quando comecei a usar o Code First do Entity Framework (EF), as classes com as quais eu planejava que o Code First funcionasse eram provavelmente 99 por cento anêmicas. A Figura 1 é um exemplo perfeito, mostrando um tipo Customer e o tipo Person do qual ele herda. Eu normalmente incluo alguns métodos, mas o tipo básico era meramente um esquema com getters e setters.

Figura 1 Classes típicas de modelo de domínio anêmico parecem tabelas de bancos de dados

public class Customer : Person
{
  public Customer()
  {
    Orders = new List<Order>();
  }
  public ICollection<Order> Orders { get; set; }
  public string SalesPersonId { get; set; }
  public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public string EmailAddress { get; set; }
  public string Phone { get; set; }
}

No final, olhei para essas classes e me dei conta de que estava fazendo um pouco mais do que definir uma tabela de banco de dados em minha classe. Isso não é necessariamente uma coisa ruim, dependendo do que você planeja fazer com o tipo.

Compare esse com o tipo Customer mais avançado listado na Figura 2, que é semelhante ao tipo Customer que explorei em minha coluna anterior (msdn.microsoft.com/magazine/dn342868). Esse tipo expõe métodos que controlam o acesso a propriedades e a outros tipos que fazem parte da agregação. Eu ajustei o tipo Customer em relação ao que você viu na coluna anterior para expressar melhor alguns dos tópicos que estou discutindo este mês.

Figura 2 Um tipo Customer que é um modelo de domínio avançado, não simplesmente propriedades

public class Customer : Contact
{
  public Customer(string firstName, string lastName, string email)
  {
    FullName = new FullName(firstName, lastName);
    EmailAddress = email;
    Status = CustomerStatus.Silver;
  }
  internal Customer()
  {
  }
  public void UseBillingAddressForShippingAddress()
  {
    ShippingAddress = new Address(
      BillingAddress.Street1, BillingAddress.Street2,
      BillingAddress.City, BillingAddress.Region,
      BillingAddress.Country, BillingAddress.PostalCode);
  }
  public void CreateNewShippingAddress(string street1, string street2,
   string city, string region, string country, string postalCode)
  {
    BillingAddress = new Address(
      street1,street2,
      city,region,
      country,postalCode)
  }
  public void CreateBillingInformation(string street1,string street2,
   string city,string region,string country, string postalCode,
   string creditcardNumber, string bankName)
  {
    BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
    CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
  }
  public void SetCustomerContactDetails
   (string email, string phone, string companyName)
  {
    EmailAddress = email;
    Phone = phone;
    CompanyName = companyName;
  }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status { get; private set; }
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get; private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}

Nesse modelo mais avançado, em vez de simplesmente expor propriedades para serem lidas e gravadas, a superfície pública de Customer é composta de métodos explícitos. Consulte a seção Setters particulares e métodos públicos da coluna do mês anterior para obter mais detalhes. Meu ponto aqui é ajudar você a compreender melhor a diferença entre a que o DDD se refere como um modelo de domínio anêmico versus avançado.

Objetos de valor podem ser confusos

Embora pareçam simples, os objetos de valor do DDD são um ponto de confusão sério para muitos, inclusive para mim. Li e ouvi sobre tantas diferentes maneiras de descrever objetos de valor a partir de várias perspectivas. Felizmente, cada uma dessas diferentes explicações, sem entrarem em conflito umas com as outras, me ajudaram a compreender mais profundamente os objetos de valor.

Essencialmente, um objeto de valor é uma classe que não tem uma chave de identidade.

O Entity Framework tem um conceito de "tipos complexos" que é muito parecido. Os tipos complexos não têm uma chave de identidade, eles são uma maneira de encapsular um conjunto de propriedades. O EF sabe como tratar esses objetos quando se trata de persistência de dados. No banco de dados, eles são armazenados como campos da tabela para a qual a entidade é mapeada.

Por exemplo, em vez de ter uma propriedade FirstName e uma propriedade LastName em sua classe Person, você poderia ter uma propriedade FullName:

public FullName FullName { get; set; }

FullName pode ser uma classe que encapsula as propriedades FirstName e LastName, com um construtor que força você a fornecer essas duas propriedades.

Mas um objeto de valor é mais do que um tipo complexo. No DDD, há três características que definem um objeto de valor:

  1. ele não tem nenhuma chave de identidade (isso se alinha com um tipo complexo)
  2. É imutável
  3. Ao verificar sua igualdade com outras instâncias do mesmo tipo, todos os seus valores são comparados

A imutabilidade é interessante. Sem uma chave de identidade, a imutabilidade de um tipo define sua identidade. Você sempre pode identificar uma instância pela combinação de suas propriedades. Como é imutável, nenhuma das propriedades do tipo podem ser alteradas, portanto, é seguro usar os valores dos tipos para identificar uma instância específica. Você já viu como as entidades do DDD se protegem contra modificações aleatórias tornando seus setters particulares, mas você poderia ter um método que permitisse afetar essas propriedades. Além de um objeto de valor ocultar os setters, ele também impede que você modifique qualquer uma das propriedades. Se você modificar uma propriedade, isso significará que o valor do objeto foi alterado. Como todo o objeto representa seu valor, você não interage com suas propriedade individualmente. Se precisar que o objeto tenha outros valores, você precisará de uma nova instância do objeto para manter um novo conjunto de valores. No final, não há como alterar uma propriedade de um objeto de valor, o que torna até mesmo a formulação de uma sentença que inclua a frase "se você precisar alterar um valor de uma propriedade" um paradoxo. Um paralelo útil é considerar uma cadeia de caracteres, que é outro tipo imutável (pelo menos em todas as linguagens com as quais tenho familiaridade. Ao trabalhar com uma instância de uma cadeia de caracteres, você não substitui caracteres individuais da cadeia de caracteres. Você simplesmente cria uma nova cadeia de caracteres.

Meu objeto de valor FullName é mostrado na Figura 3. Ele não tem nenhuma propriedade de chave de identidade. Você pode ver que a única maneira de criar uma instância dele é fornecer os dois valores das propriedades e não há como modificar nenhuma dessas propriedades. Portanto, ele atende ao requisito de imutabilidade. O requisito final, de que ele forneça uma maneira de comparar a igualdade com outra instância desse tipo, está oculto na classe personalizada ValueObject (que emprestei de Jimmy Bogard em bit.ly/13SWd9h) da qual ele herda, porque esse é um monte de código intricado. Embora esse ValueObject não leve em conta propriedades de coleção, ele atende às minhas necessidades porque não tenho nenhuma coleção nesse objeto de valor.

Figura 3 O objeto de valor FullName

public class FullName:ValueObject<FullName>
{
  public FullName(string firstName, string lastName)
  {
    FirstName = firstName;
    LastName = lastName;
  }
  public FullName(FullName fullName)
    : this(fullName.FirstName, fullName.LastName)
  {    }
  internal FullName() { }
  public string FirstName { get; private set; }
  public string LastName { get; private set; }
 // More methods, properties for display formatting
}

Tendo em mente que você pode precisar de uma cópia modificada, por exemplo, semelhante a esse FullName, mas com outro FirstName, Vaughn Vernon, autor de “Implementing Domain-Driven Design” (Addison-Wesley Professional, 2013), sugere que FullName poderia incluir métodos para criar novas instâncias a partir da instância existente:

public FullName WithChangedFirstName(string firstName)
{
  return new FullName(firstName, this.LastName);
}
public FullName WithChangedLastName(string lastName)
{
  return new FullName(this.FirstName, lastName);
}

Quando adiciono em minha camada de persistência (Entity Framework), isso será visto como uma propriedade ComplexType do EF de qualquer classe no qual seja usado. Quando uso FullName como uma propriedade de meu tipo Person, o EF armazenará as propriedades de FullName na tabela do banco de dados onde o tipo Person está armazenado. Por padrão, as propriedades serão nomeadas FullName_FirstName e FullName_LastName no banco de dados.

FullName é bastante direto. Mesmo que estivesse criando design focalizado em banco de dados, você não armazenaria FirstName e LastName em uma tabela separada.

Objetos de valor ou objetos relacionados?

Mas agora, considere outro cenário, imagine um Customer com um ShippingAddress e um BillingAddress:

public Address ShippingAddress { get; private set; }
public Address BillingAddress { get; private set; }

Por padrão (cérebro controlado por dados), crio Address como uma Entidade, incluindo uma propriedade AddressId. E, mais uma vez, como "penso em dados", suponho que Address será armazenado como uma tabela separada no banco de dados. OK, estou me esforçando para parar de considerar o banco de dados ao modelar meu domínio, portanto, isso não tem importância. No entanto, em algum ponto, adiciono minha camada de dados usando o EF, e o EF também fará essa suposição. Mas o EF não poderá resolver os mapeamentos corretamente. Ele presumirá uma relação de 0..1:* entre o Address e o Customer. Ou seja, que um endereço deve ter qualquer número de clientes relacionados e um Customer deve ter um ou nenhum endereço.

Meu DBA talvez não goste desse resultado e, o que é mais importante, não escreverei o código de meu aplicativo com base na mesma suposição que o Entity Framework está fazendo, de que há muitos Customers para zero ou um Address. Por causa disso, o EF pode afetar a persistência ou a recuperação de meus dados de algumas maneiras inesperadas. Portanto, da perspectiva de alguém que tem trabalhado muito com o EF, minha primeira abordagem é corrigir os mapeamentos do EF usando a API fluente. Mas pedir ao EF para corrigir esse problema me forçaria a adicionar propriedades de Address apontando para Customer, o que não desejo em meu modelo. Como você pode ver, resolver esse problema com mapeamentos do EF me levaria a uma aventura no desconhecido.

Quando recuo e focalizo o domínio, não o EF ou o banco de dados, faz mais sentido simplesmente tornar Address um objeto de valor em vez de uma entidade. São necessárias três etapas:

  1. preciso remover a propriedade principal do tipo Address (provavelmente chamada Id ou AddressId).
  2. Preciso ter certeza de que Address é imutável. Seu construtor já permite popular todos os campos. Preciso remover todos os métodos que permitam que qualquer propriedade seja alterada.
  3. Preciso garantir que a igualdade de Address possa ser verificada com base nos valores de suas propriedades e campos. Posso fazer isso herdando mais uma vez daquela boa classe ValueObject que emprestei de Jimmy Bogard (consulte a Figura 4).

Figura 4 Address como um objeto de valor

public class Address:ValueObject<Address>
{
  public Address(string street1, string street2, 
    string city, string region,
    string country, string postalCode) {
    Street1 = street1;
    Street2 = street2;
    City = city;
    Region = region;
    Country = country;
    PostalCode = postalCode;  }
  internal Address()  {  }
  public string Street1 { get; private set; }
  public string Street2 { get; private set; }
  public string City { get; private set; }
  public string Region { get; private set; }
  public string Country { get; private set; }
  public string PostalCode { get; private set; }
  // ... 
}

A maneira como uso essa classe Address a partir de minha classe Customer não será alterada, com a exceção de que se eu precisar modificar o endereço, precisarei criar uma nova instância.

Mas agora não preciso mais me preocupar com o gerenciamento dessa relação. E isso também fornece outro teste decisivo para objetos de valor, uma vez que um Customer é realmente definido por suas informações de remessa e de cobrança porque, se eu vender qualquer coisa para esse cliente, muito provavelmente ela precisará ser enviada, e o departamento de contas a pagar precisará do endereço de cobrança.

Isso não quer dizer que toda relação de 1:1 ou de 1:0..1 possa ser substituída por objetos de valor, mas é uma boa solução aqui, e meu trabalho se tornou muito mais simples quando parei de tentar resolver um quebra-cabeça depois do outro que surgia quando eu tentava forçar o Entity Framework a manter essa relação para mim.

Como o exemplo de FullName, a alteração de Address para um objeto de valor significa que o Entity Framework verá Address como um ComplexType e armazenará todos esses dados nas tabelas de Customer. Os muitos anos trabalhando com normalização de banco de dados me fizeram pensar automaticamente que isso seria uma coisa ruim, mas meu banco de dados pode tratar isso facilmente, e isso funciona para meu domínio específico.

Posso me lembrar de muitos argumentos contra o uso dessa técnica, mas todos eles começam com "e se". Todos os que não dizem respeito a meu domínio não são argumentos válidos. Gastamos muito tempo codificando para cenários prováveis que nunca ocorrem. Estou tentando ser mais cuidadosa em relação a adicionar essas soluções preventivas em minhas soluções.

Ainda não concluído

Estou realmente passando pela lista de conceitos de DDD que assombram esta geek de dados. Conforme compreendo mais completamente esses conceitos, quero saber ainda mais. Depois de mudar um pouco meu raciocínio, esses padrões fazem muito sentido para mim. Eles não reduzem nem um pouco meu interesse na persistência de dados, mas separar o domínio da persistência de dados e da infraestrutura parece a coisa certa para mim, depois de ter brigado com eles por tantos anos. Ainda assim, é importante lembrar que há uma grande quantidade de atividades de software que não precisam de DDD. O DDD pode ajudar a destrinchar problemas complexos, mas, com frequência, é excessivo para os simples.

Na próxima coluna, discutirei algumas outras estratégias técnicas do DDD que inicialmente também pareciam entrar em conflito com meu raciocínio de dados em primeiro lugar, como desistir de relações bidirecionais, considerar invariáveis e lidar com qualquer necessidade visível de disparar acesso a dados a partir de suas agregações.

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 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)
Stephen A. Bohlen é divulgador sênior de tecnologia da Microsoft Corporation e utiliza sua experiência variada de mais de 20 anos como arquiteto, gerente de CAD, tecnólogo de TI, engenheiro de software, CTO (diretor de tecnologia) e consultor para ajudar as organizações de parceiros da Microsoft na adoção de produtos e tecnologias de desenvolvedor avançados e de pré-lançamento da Microsoft.