The Cutting Edge

Criação de um modelo de domínio

Dino Esposito

 

Dino EspositoA versão mais recente do Entity Framework 4.1 e o novo padrão de desenvolvimento Code First quebram uma regra fundamental do desenvolvimento de servidores: não dê nenhum passo se o banco de dados não estiver implementado. O Code First orienta os desenvolvedores a se concentrarem no domínio corporativo e modelá-lo em termos de classes. De certo modo, o Code First encoraja a aplicação de princípios de design orientado a domínio (DDD) no espaço do .NET. Um domínio corporativo é preenchido com entidades relacionadas e interconectadas, cada uma das quais tem seus próprios dados expostos como propriedades e pode expor um comportamento através de métodos e eventos. E mais importante ainda, cada entidade pode ter um estado e ser associada a uma lista possivelmente dinâmica de regras de validação.

Criar um modelo de objeto para um cenário realista levanta algumas questões que não são abordadas em demonstrações e tutoriais atuais. Neste artigo, vou enfrentar o desafio e discutir a criação de uma classe de cliente, abordando uma série de padrões e práticas de design durante o processo, tais como o padrão Party, raízes agregadas, alocadores e tecnologias como contratos de código e o Enterprise Library Validation Application Block (VAB).

Como referência, recomendo que você consulte um projeto de código-fonte aberto do qual o código aqui discutido é um pequeno subconjunto. Criado por Andrea Saltarello, o projeto Northwind Starter Kit (nsk.codeplex.com) visa ilustrar práticas eficazes de arquitetura de soluções em multicamadas.

Modelo de objeto versus modelo de domínio

Debater sobre a escolha de um modelo de objeto ou um modelo de domínio pode parecer inútil e, na maioria das vezes, é somente uma questão de terminologia. Mas terminologia precisa é um fator importante de garantia que todos os membros de uma equipe tenham o mesmo conceito em mente ao usar determinados termos.

Para praticamente todos na indústria de software, um modelo de objeto é uma coleção de objetos genéricos, mas possivelmente relacionados. E como um modelo de domínio difere disso? No final, um modelo de domínio é ainda um modelo de objeto, de modo que usar os dois termos de forma intercambiável pode não ser um grande engano. Mesmo assim, quando o termo “domínio de modelo” é usado com certa ênfase, ele pode trazer consigo uma dose de expectativas quanto à forma dos objetos de que é constituído.

Esse uso de modelo de domínio está associado à definição dada por Martin Fowler: um modelo de objeto do domínio que incorpora comportamento e dados. Por sua vez, o comportamento expressa regras e lógica específica (consulte bit.ly/6Ol6uQ).

O DDD acrescenta várias regras pragmáticas a um modelo de domínio. De acordo com essa perspectiva, um modelo de domínio difere de um modelo de objeto no uso intensivo de objetos de valor que ele recomenda, no lugar de tipos primitivos. Um inteiro, por exemplo, pode ser muitas coisas: uma temperatura, uma quantia em dinheiro, um tamanho, uma quantidade. Um modelo de domínio usaria um objeto de valor específico para cada cenário diferente.

Além disso, um modelo de domínio deveria identificar raízes agregadas. Uma raiz agregada é uma entidade obtida pela composição de outras entidades. Objetos na raiz agregada não têm relevância externa, o que significa que não há casos de uso nos quais eles são usados sem serem passados a partir do objeto de raiz. O exemplo canônico de uma raiz agregada é a entidade Order. Order contém OrderItem como agregado, mas não Product. É difícil imaginar (embora isso devesse ser determinado somente por suas especificações) que você precisaria trabalhar com uma OrderItem sem que ela viesse de uma Order. Por outro lado, pode muito bem haver casos em que você trabalhe com entidades Product que não envolvam pedidos. Raízes agregadas são responsáveis por manter seus objetos filhos em um estado válido e por persisti-los.

Por fim, algumas classes de domínio podem oferecer métodos públicos alocadores para criar novas instâncias, no lugar de construtores. Quando a classe é principalmente autônoma e, na realidade, não faz parte de uma hierarquia ou quando as etapas para criar a classe são de algum interesse do cliente, o uso de um construtor simples é aceitável. Com objetos complexos como raízes agregadas, entretanto, você precisa de um nível adicional de abstração sobre a instanciação. O DDD introduz objetos alocadores (ou, mais simplesmente, métodos alocadores em algumas classes) como um modo para desassociar requisitos de cliente dos objetos internos e seus relacionamentos e regras. Uma introdução muita clara e concisa pode ser encontrada em bit.ly/oxoJD9.

O padrão Party

Vamos nos concentrar em uma classe Customer. Em vista do que foi dito anteriormente, segue uma possível assinatura:

public class Customer : Organization, IAggregateRoot
{
  ...
}

Quem é seu cliente? Ele é um indivíduo, uma organização ou ambos? O padrão Party sugere que você distinga entre os dois e defina claramente que propriedades são comuns e quais pertencem somente a indivíduos ou organizações. O código na Figura 1 limita-se a Person e Organization; você pode torná-lo mais detalhado classificando as organizações em companhias não lucrativas e comerciais se o seu domínio corporativo assim o exigir.

Figura 1 Classes de acordo com o padrão Party

public abstract class Party
{
  public virtual String Name { get; set; }
  public virtual PostalAddress MainPostalAddress { get; set; }
}
public abstract class Person : Party
{
  public virtual String Surname { get; set; }
  public virtual DateTime BirthDate { get; set; }
  public virtual String Ssn { get; set; }
}
public abstract class Organization : Party
{
  public virtual String VatId { get; set; }
}

Nunca é demais lembrar que você deve visar à produção de um modelo que modele fielmente seu real domínio corporativo e não uma representação abstrata do negócio. Se seus requisitos se referem somente a clientes como indivíduos, então aplicar o padrão Party não é estritamente necessário, embora ele introduza um ponto para futura extensibilidade. 

Customer como uma classe de raiz agregada

Uma raiz agregada é uma classe em seu modelo que representa uma entidade autônoma. Uma classe que não existe em relação a outras entidades. Na maioria das vezes, você tem raízes agregadas que são somente classes individuais que não gerenciam nenhum objeto filho ou, talvez, simplesmente apontam para a raiz de outros agregados. A Figura 2 mostra um pouco mais sobre a classe Customer.

Figura 2 Classe Customer como uma raiz agregada

public class Customer : Organization, IAggregateRoot
{
  public static Customer CreateNewCustomer(
    String id, String companyName, String contactName)
  {
    ...
  }
 
  protected Customer()
  {
  }
 
  public virtual String Id { get; set; }
    ...
 
  public virtual IEnumerable<Order> Orders
  {
    get { return _Orders; }
  }
   
  Boolean IAggregateRoot.CanBeSaved
  {
    get { return IsValidForRegistration; }
  }
 
  Boolean IAggregateRoot.CanBeDeleted
  {
    get { return true; }
  }
}

Como você pode observar, a classe Customer implementa a interface (personalizada) IAggregateRoot. Esta é a interface:

public interface IAggregateRoot
{
  Boolean CanBeSaved { get; }
  Boolean CanBeDeleted { get; }
}

O que significa ser uma raiz agregada? Uma raiz agregada lida com persistência para seus objetos filhos agregados e é responsável por impor condições invariáveis que envolvam o grupo. Acontece que uma raiz agregada deve ser capaz de verificar se toda a pilha pode ser salva ou excluída. Uma raiz agregada autônoma simplesmente retorna verdadeiro, sem nenhuma verificação adicional.

Alocador e construtor

Um construtor é específico de tipo. Se o objeto for somente um tipo, sem agregados nem lógica de inicialização complexa, o uso de um construtor simples será mais que aceitável. Em geral, entretanto, um alocador é uma camada útil e extra de abstração. Um alocador pode ser um método simples e estático na classe de entidade ou o próprio componente isolado. Ter um método alocador ajuda também a legibilidade, pois ele esclarece porque você está criando aquela determinada instância. Com construtores, sua capacidade de abordar diferentes cenários de instanciação é mais limitada, uma vez que construtores não são métodos nomeados e somente podem ser distinguidos pela assinatura. Especialmente com assinaturas longas, é difícil descobrir, mais tarde, porque uma determinada instância está sendo obtida. A Figura 3 mostra o método alocador na classe Customer.

Figura 3 Método alocador na classe Customer

public static Customer CreateNewCustomer(
  String id, String companyName, String contactName)
{
  Contract.Requires<ArgumentNullException>(
           id != null, "id");
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(id), "id");
  Contract.Requires<ArgumentNullException>(
           companyName != null, "companyName");
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(companyName), "companyName");
  Contract.Requires<ArgumentNullException>(
           contactName != null, "contactName");               
  Contract.Requires<ArgumentException>(
           !String.IsNullOrWhiteSpace(contactName), "contactName");
 
  var c = new Customer
              {
                Id = id,
                Name = companyName,
                  Orders = new List<Order>(),
                ContactInfo = new ContactInfo
                              {
                                 ContactName = contactName
                              }
              };
  return c;
}

O método alocador é atômico, obtém parâmetros de entrada, executa sua tarefa e retorna uma instância atualizada de um determinado tipo. A instância que está sendo retornada deve ser garantida como estando em um estado válido. O alocador é responsável por satisfazer todas as regras internas de validação.

O alocador também precisa validar os dados de entrada. Para tal, o uso de pré-condições de contratos de código mantém o código limpo e totalmente legível. Você pode também usar pós-condições para garantir que a instância retornada esteja em um estado válido, da seguinte forma:

Contract.Ensures(Contract.Result<Customer>().IsValid());

Quanto ao uso de invariáveis na classe, a experiência mostra que nem sempre é possível. Invariáveis podem ser muito invasivas, especialmente em modelos grandes e complexos. Invariáveis de contratos de código são, às vezes, quase que muito obedientes ao conjunto de regras e há vezes em que você deseja mais flexibilidade em seu código. É preferível, então, restringir as áreas onde as invariáveis devem ser impostas.

Validação

As propriedades em uma classe de domínio precisam ser avaliadas para garantir que nenhum campo obrigatório fique em branco, que nenhum texto muito longo seja colocado em contêineres de tamanho limitado, que os valores estejam dentro dos intervalos adequados e assim por diante. Você deverá também considerar validação de propriedades cruzadas e regras corporativas sofisticadas. Como você codificaria a validação? 

Validation tem a ver com codificação condicional, de modo que, no final, é uma questão de combinar algumas instruções e retornar alguns booleanos. Escrever uma camada de validação com código simples e sem nenhuma estrutura ou tecnologia pode funcionar, mas não é bem uma boa ideia. O código resultante não seria muito legível e não seria de fácil evolução, embora algumas bibliotecas fluentes estejam tornando isso mais fácil. Sujeita a regras corporativas reais, a validação pode ser muito volátil e sua implementação deve levar isso em conta. No fim, você não pode simplesmente escrever um código que valide; você precisa escrever um código que seja aberto à validação dos mesmos dados face a regras diferentes.

Com a validação, algumas vezes você deseja alertar quando dados inválidos são passados e, outras vezes, você só quer coletar erros e relatá-los a outras camadas de código. Lembre-se que contratos de código não validam. Eles verificam condições e depois levantam uma exceção se uma condição não se aplicar. Usando um manipulador de erro centralizado você pode recuperar de exceções e degradar naturalmente. Em geral, eu recomendo usar contratos de código em uma entidade de domínio somente para identificar potenciais erros graves que possam levar a estados inconsistentes. Faz sentido usar contratos de código em um alocador. Nesse caso, se dados passados são inválidos, o código deve alertar. Usar ou não contratos de código em métodos setter é prerrogativa sua. Eu prefiro pegar um caminho mais tranquilo e validar via atributos. Mas que atributos?

Data Annotations versus VAB

O namespace Data Annotations e a o Enterprise Library VAB são muito semelhantes. Ambas as estruturas são baseadas em atributo e podem ser estendidas com classes personalizadas representando regras personalizadas. Em ambos os casos, você pode definir validação de propriedades cruzadas. Por fim, ambas as estruturas têm uma API validadora que avalia um instância e retorna a lista de erros. Onde está a diferença?

Data Annotations é parte do Microsoft .NET Framework e não requer um download separado. Enterprise Library é um download separado. Nada muito importante em um projeto grande, mas ainda assim um problema, uma vez que requer aprovação em cenários corporativos. Enterprise Library pode ser facilmente instalado via NuGet (consulte o artigo sobre gerenciamento de bibliotecas de projeto com o NuGet, nesta edição).

O Enterprise Library VAB é superior ao Data Annotations em um aspecto: Ele pode ser configurado via conjuntos de regras XML. Um conjunto de regras XML é uma entrada no arquivo de configuração onde você descreve a validação desejada. Não é necessário dizer que você pode alterar itens de maneira declarativa, sem nem tocar em seu código. A Figura 4 mostra um exemplo de conjunto de regras.

Figura 4 Conjuntos de regras do Enterprise Library

<validation>
   <type assemblyName="..." name="ValidModel1.Domain.Customer">
     <ruleset name="IsValidForRegistration">
       <properties>
         <property name="CompanyName">
           <validator negated="false"
                      messageTemplate="The company name cannot be null" 
                      type="NotNullValidator" />
           <validator lowerBound="6" lowerBoundType="Ignore"
                      upperBound="40" upperBoundType="Inclusive" 
                      negated="false"
                      messageTemplate="Company name cannot be longer ..."
                      type="StringLengthValidator" />
         </property>
         <property name="Id">
           <validator negated="false"
                      messageTemplate="The customer ID cannot be null"
                      type="NotNullValidator" />
         </property>
         <property name="PhoneNumber">
           <validator negated="false"
                      type="NotNullValidator" />
           <validator lowerBound="0" lowerBoundType="Ignore"
                      upperBound="24" upperBoundType="Inclusive"
                      negated="false"
                      type="StringLengthValidator" />
         </property>
         <property name="FaxNumber">
           <validator negated="false"
                      type="NotNullValidator" />
           <validator lowerBound="0" lowerBoundType="Ignore"
                      upperBound="24" upperBoundType="Inclusive"
                      negated="false"
                      type="StringLengthValidator" />
         </property>
       </properties>
     </ruleset>
   </type>
 </validation>

Um conjunto de regras relaciona os atributos que você deseja aplicar a uma determinada propriedade em um determinado tipo. Em código, você valida um conjunto de regras da seguinte maneira:

public virtual ValidationResults ValidateForRegistration()
{
  var validator = ValidationFactory
          .CreateValidator<Customer>("IsValidForRegistration");
  var results = validator.Validate(this);
  return results;
}

O método aplica validadores relacionados no conjunto de regras IsValidForRegistration à instância especificada.

Um último comentário sobre validação e bibliotecas. Eu não abordei todas as bibliotecas de validação mais populares, mas isso não faz uma grande diferença. O ponto importante a considerar é se as suas regras corporativas mudam e com que frequência elas mudam. Baseado nisso, você pode decidir se Data Annotations, VAB, contratos de código ou alguma outra biblioteca é o mais adequado. Pela minha experiência, se você souber exatamente o que precisa atingir, então será fácil escolher a biblioteca de validação “correta”.

Conclusão

Um modelo de objeto para um domínio corporativo realístico dificilmente será uma coleção de propriedades e classes. Além disso, considerações de design têm precedência sobre tecnologias. Um modelo de objeto bem feito expressa cada aspecto necessário do domínio. Na maioria das vezes, isso implica em ter classes que sejam fáceis de inicializar e validar e que sejam sofisticadas em termos de propriedades e lógica. Práticas de DDD não devem ser consideradas de maneira dogmática, mas, em vez disso, devem ser as balizas que demarcam o caminho a ser seguido.     

Dino Esposito* é o autor de “Programming Microsoft ASP.NET 4” (Microsoft Press, 2011) e “Programming Microsoft ASP.NET MVC” (Microsoft Press, 2011) e coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2008). Residente na Itália, Esposito é um palestrante sempre presente em eventos do setor no mundo inteiro. Siga-o no Twitter em twitter.com/despos.*

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Manuel Fahndrich eAndrea Saltarello