Pontos de dados

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

Julie Lerman

Baixar o código de exemplo

Julie LermanEste ano, o livro de design de software inovador de Eric Evans, “Domain-Driven Design: Tackling Complexity in the Heart of Software” (Addison-Wesley Professional, 2003, amzn.to/ffL1k), celebra o seu décimo aniversário. Evans empregou nesse livro muitos anos de experiência na orientação de grandes empresas no processo de criação de software. Depois, ele passou mais alguns anos pensando em como encapsular os padrões que levam esses projetos aos sucesso — interagir com o cliente, analisar os problemas de negócios resolvidos, montar equipes e arquitetar o software. O foco desses padrões é o domínio da empresa, e juntos compõem o DDD (Design controlado por domínio). Com o DDD, você pode modelar o domínio em questão. Os padrões resultam dessa abstração de seu conhecimento sobre o domínio. Reler o prefácio de Martin Fowler e de Evans, mesmo hoje, continua a fornecer uma visão geral rica da essência do DDD.

Nesta coluna e nas próximas duas também, vou compartilhar algumas dicas que ajudaram meu cérebro focado em dados do Entity Framework a esclarecer as ideias enquanto trabalho em fazer com que meu código se beneficie de alguns padrões técnicos do DDD.

Por que me importo com o DDD?

Minha introdução ao DDD veio de uma curta entrevista em vídeo no InfoQ.com com Jimmy Nilsson, um respeitado arquiteto na comunidade .NET (e em outros lugares), que falava sobre o LINQ para SQL e o Entity Framework (bit.ly/11DdZue). No final, pedem para Nilsson citar seu livro de tecnologia favorito. Ele responde: “Meu livro favorito sobre computador é o livro de Eric Evans, “Domain-Driven Design”. É como poesia, eu acho. Não é só o conteúdo é excelente, mas você pode lê-lo muitas vezes e lê-se como a poesia.” Poesia! Eu estava escrevendo meu primeiro livro de tecnologia, “Programming Entity Framework” (O’Reilly Media, 2009), na época, e fiquei intrigada com essa descrição. Então, fui ler um pouco de livro de Evans para ver como era. Evans é um escritor fluente e interessante. E essa visão naturalista do desenvolvimento de software, combinada com sua visão perspicaz, torna a leitura do livro prazerosa. Mas também fiquei surpresa com o que estava lendo. Não só a redação era maravilhosa, mas o assunto que ele abordava me intrigou. Ele falava sobre a construção de relacionamentos com os clientes e realmente entender seus negócios e seus problemas (relacionados ao software em questão), não apenas trabalhar arduamente no código. Essa questão sempre foi importante para mim em todos meus 25 anos de desenvolvimento de software. Que queria mais.

Eu passeei pelo campo de DDD por mais alguns anos e depois comecei a aprender mais — conhecendo Evans em uma conferência e depois participando de seu workshop de imersão de quatro dias. Embora eu esteja longe especialista em DDD, achei que o padrão Contexto Limitado era algo que eu poderia usar imediatamente enquanto trabalhava para deixar meu próprio processo de criação de software com uma estrutura mais organizada e gerenciável. Você pode ler sobre esse assunto na minha coluna de janeiro de 2013, “Reduza os modelos EF com Contextos Limitados DDD” (msdn.microsoft.com/magazine/jj883952).

Desde então, explorei ainda mais o assunto. Estou intrigada e inspirada pelo DDD, mas luto com minha perspectiva orientada por dados para compreender alguns padrões técnicos que fazem dela um sucesso. É bem provável que muitos desenvolvedores passem pela mesma dificuldade, então vou compartilhar algumas lições que aprendi com a ajuda, o interesse e a generosidade de Evans e de vários outros praticantes e professores do DDD, incluindo Paul Rayner, Vaughn Vernon, Greg Young, Cesar de la Torre e Yves Reynhout.

Na modelagem do domínio, esqueça a persistência

A modelagem do domínio é inteiramente focada nas tarefas da empresa. Ao projetar tipos e suas propriedades e comportamentos, fico tentada a pensar em como uma relação funcionará no banco de dados e como minha estrutura preferida de mapeamento relacional de objeto (ORM) — Entity Framework — tratará as propriedades, relações e hierarquias de herança que estou criando. A menos que você está criando um software para uma empresa cujo ramo de atividade é a recuperação e o armazenamento de dados — como o Dropbox — a persistência de dados desempenha um papel apenas de apoio em seu aplicativo. É como fazer uma chamada externa para a API de uma fonte de previsão do tempo para exibir a temperatura atual para um usuário. Ou enviar dados de seu aplicativo para um serviço externo, talvez um registro no Meetup.com. É claro que seus dados podem ser mais complicados, mas com a abordagem DDD para contextos limitados, focando em comportamentos e seguindo orientação do DDD para criar tipos, a persistência pode ser bem menos complexa do que os sistemas que você pode estar criando atualmente.

E se você já estudou seu ORM, como aprender a configurar mapeamentos de bancos de dados usando a API fluente do Entity Framework, você será capaz de fazer a persistência funcionar conforme necessário. Na pior das hipóteses, você pode precisar fazer alguns ajustes em suas classes. Em um caso extremo, como com um banco de dados antigo, você pode até mesmo adicionar um modelo de persistência projetado para mapeamento de banco de dados e depois usar algo como o AutoMapper para resolver as coisas entre seu modelo de domínio e seu modelo de persistência.

Mas essas preocupações estão relacionadas ao problema de negócios que seu software deve resolver. Então, a persistência não deve interferir no design do domínio. Isso é um desafio para mim, porque como eu estou projetando minhas entidades, não posso deixar de considerar como o EF irá inferir em seus mapeamentos de banco de dados. Então, eu tento bloquear esse ruído.

Setters particulares e métodos públicos

Outra regra básica é tornar particular a propriedade setters. Em vez de permitir que o código de chamada defina aleatoriamente várias propriedades, você deve controlar a interação com objetos DDD e seus dados relacionados usando métodos que modificam as propriedades. E, não, não me refiro a métodos como SetFirstName e SetLastName. Por exemplo, em vez de instanciar um novo tipo Customer e depois definir cada uma das suas propriedades, você pode ter algumas regras a considerar ao criar um novo cliente. Você pode criar essas regras no construtor do Customer, usar um método Factory Pattern ou até mesmo ter um método Create no tipo Customer. A Figura 1 mostra um tipo Customer que é definido seguindo o padrão DDD de uma raiz agregada (isto é, o "pai" de um gráfico de objetos, também referido como "entidade raiz" no DDD). As propriedades Customer têm setters particulares para que somente outros membros Customer possam afetar diretamente essas propriedades. A classe expõe um construtor para controlar como ela é instanciada e oculta o construtor sem parâmetros (exigido pelo Entity Framework) como interno.

Figura 1 Propriedades e métodos de um tipo que age como raiz agregada

public class Customer : Contact
{
  public Customer(string firstName,string lastName, string email)
  { ... }
  internal Customer(){ ... }
  public void CopyBillingAddressToShippingAddress(){ ... }    
  public void CreateNewShippingAddress(
   string street, string city, string zip) { ... }
  public void CreateBillingInformation(
   string street, string city, string zip,
   string creditcardNumber, string bankName){ ... }    
  public void SetCustomerContactDetails(
   string email, string phone, string 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; }
}

O tipo Customer controla e protege as outras entidades no agregado — alguns endereços e um tipo de cartão de crédito — expondo métodos específicos (como CopyBillingAddressToShippingAddress) com os quais esses objetos serão criados e manipulados. A raiz agregada deve garantir que as regras que definem cada entidade no agregado sejam aplicadas usando a lógica de domínio e o comportamento implementados em um desses métodos. Mais importante, a raiz agregada é responsável pela lógica invariável e a consistência em todo o agregado. Falarei mais sobre invariáveis na minha próxima coluna, mas por enquanto, recomendo a leitura de post do blog de Jimmy Bogard, “Strengthening Your Domain: Aggregate Construction”, em bit.ly/ewNZ52, que fornece uma excelente explicação das invariáveis em agregados.

No final, o que é exposto por Customer é o comportamento, e não as propriedades: CopyBillingAddressToShippingAddress, CreateNewShipping­Address, CreateBillingInformation e SetCustomerContactDetails.

Observe que o tipo Contact, do qual Customer deriva, habita em um assembly diferente chamado "Common", pois pode ser exigido por outras classes. Preciso esconder as propriedades do Contact, mas elas não podem ser particulares ou Customer não seria capaz de acessá-los. Em vez disso, elas estão no escopo como Protected:

public class Contact: Identity
{
  public string CompanyName { get; protected set; }
  public string EmailAddress { get; protected set; }
  public string Phone { get; protected set; }
}

Uma observação sobre identidades: Customer e Contact podem parecer objetos de valor DDD porque não têm valor de chave. No entanto, em minha solução, o valor da chave é fornecido pela classe Identity, da qual Contact deriva. E nenhum desses tipos são imutáveis, então, de qualquer forma, eles não podem ser considerados objetos de valor.

Como Customer herda de Contact, ele terá acesso a essas propriedades protegidas e poderá defini-las, como neste método SetCustomerContactDetails:

public void SetCustomerContactDetails (string email, string phone, string companyName)

{

  EmailAddress = email;

  Phone = phone;

  CompanyName = companyName;

}

Às vezes tudo que você precisa é do CRUD

Nem tudo em seu aplicativo precisa ser criado usando o DDD. O DDD serve para ajudar a lidar com comportamentos complexos. Se você precisa fazer apenas uma edição ou consulta bruta aleatória, então uma simples classe (ou um conjunto de classes), definida como você normalmente o faria com o EF Code First (usando propriedades e relações) e combinada com os métodos insert, update e delete (em um repositório ou apenas DbContext), é tudo que você precisa. Então, para realizar algo como criar uma ordem e seus itens de linha, convém usar o DDD para ajudar a trabalhar com regras e os comportamentos de negócios especiais. Por exemplo, é um cliente Gold Star que está fazendo o pedido? Nesse caso, você precisa obter alguns detalhes dos clientes para determinar se a resposta é sim e, se for, aplicar 10% de desconto para cada item a ser adicionado ao pedido. O usuário forneceu suas informações de cartão de crédito? Então, talvez seja necessário chamar um serviço de verificação para confirmar se o cartão é válido.

O segredo no DDD é incluir a lógica de domínio como métodos nas classes de entidade do domínio, usando o OOP em vez de implementar "scripts transacionais" dentro de objetos de comerciais sem monitoração de estado, que tem o mesmo formado de uma típica classe Code First de demonstração. 

Mas às vezes tudo que você está fazendo é algo muito normal, como criar um registro de contato: nome, endereço, referência etc., e salvá-lo. É só criar, ler, atualizar e excluir (CRUD). Você não precisa criar agregados e raízes e comportamentos para cumprir esse princípio.

Provavelmente, seu aplicativo conterá uma combinação de comportamentos complexos e CRUD simples. Aproveite o tempo para esclarecer os comportamentos e não desperdice tempo, energia e dinheiro arquitetando demais os componentes de seu aplicativo que são realmente bem simples. Nesses casos, é importante identificar os limites entre diferentes subsistemas ou contextos limitados. Um contexto limitado poderia ser muito orientado a dados (apenas CRUD), ao passo que um contexto limitado ao domínio central fundamental deve ser projetado seguindo abordagens do DDD.

Dados compartilhados podem ser uma maldição em sistemas complexos

Outra questão com a qual quebrei a cabeça, e depois esbravejei e reclamei quando as pessoas gentilmente tentavam explicar mais detalhes, refere-se ao compartilhamento de tipos e dados entre subsistemas. Ficou claro que eu não podia "fazer o bolo e comê-lo". Então, fui forçada a pensar novamente sobre minha suposição de que eu deveria compartilhar positivamente os tipos entre sistemas e fazer com que todos esses interagissem com a mesma tabela no mesmo banco de dados.

Estou aprendendo a considerar realmente onde eu preciso compartilhar dados e escolher as minhas batalhas. Algumas coisas simplesmente não valem a pena tentar, como mapear de contextos diferentes para uma única tabela ou até mesmo um único banco de dados. O exemplo mais comum é compartilhar um tipo Contact que está tentando satisfazer às necessidades de todas as pessoas, em todos os sistemas. Como você pode conciliar e usar o controle de origem para um tipo Contact que pode ser necessário em vários sistemas? E se um sistema precisar modificar a definição desse tipo Contact? No que diz respeito um ORM, como mapear um Contact usado em todos sistemas para uma única tabela em um único banco de dados?

O DDD afasta você do compartilhamento de modelos de domínio e dados, explicando que você não precisa sempre apontar para a mesma tabela de pessoas em um único banco de dados.

Meu maior receio com isso é baseado em 25 anos de foco nos benefícios da reutilização — reutilização de código e reutilização de dados. Então, sofri com a seguinte ideia mas estou amadurecendo: não é um crime duplicar os dados. Nem todos os dados caberão neste paradigma novo (para mim), é claro. Mas que tal algo leve como o nome de uma pessoa? Então, e se você duplicar o nome e o sobrenome da pessoa em várias tabelas ou até mesmo vários bancos de dados que são dedicados a diferentes subsistemas da sua solução de software? A longo prazo, esquecendo a complexidade do compartilhamento de dados, você tornará o trabalho de criação de seu sistema muito mais simples. Em qualquer caso, você deve sempre minimizar a duplicação de dados e atributos em diferentes contextos limitados. Às vezes, você só precisa da ID e do status do cliente para calcular descontos em um contexto limitado a preço. O nome e sobrenome do mesmo cliente podem ser necessários somente no contexto limitado ao gerenciamento de contatos.

Mas há ainda muitas informações que precisam ser compartilhadas entre os sistemas. Você pode usar o que o DDD chama de "camada anticorrupção" (que pode ser algo tão simples quanto um serviço ou uma fila de mensagens) para garantir que, por exemplo, se alguém criar um novo contato em um sistema, você reconheça que a pessoa já existe em outro lugar, ou garantir que a pessoa, juntamente com uma chave de identidade comum, seja criada em outro subsistema.

Há muito para mastigar no mês que vem

Estando eu na fase de aprimorar meu aprendizado e compreensão do lado técnico do DDD, lutando para reconciliar velhos hábitos com novas ideias e chegando em inúmeros momentos “Eureca!”, os ponteiros que abordei neste artigo são verdadeiramente aqueles que me ajudaram e encontrar a luz no fim do túnel. Às vezes, é só uma questão de perspectiva, e a maneira como as expressei aqui reflete a perspectiva que tornou as coisas mais claras para mim.

Vou compartilhar mais alguns momentos “Eureca!” na minha próxima coluna, onde falarei sobre esse termo condescendente que você já deve ter ouvido: “modelo de domínio anêmico”, juntamente com seu primo DDD, o “modelo de domínio rico”. Também falarei sobre relações unidirecionais e o que esperar quando chega a hora de adicionar persistência de dados se você estiver usando o Entity Framework. Eu também falarei sobre alguns tópicos relacionados ao DDD mais que me causaram muita dor de cabeça em um esforço para encurtar sua própria curva de aprendizado.

Até lá, por que não olhar detalhadamente suas próprias classes e ver como ser mais que um maníaco por controle, ocultando os setters de propriedade e expondo métodos mais descritivos e explícitos. E lembre-se: nenhum método “SetLastName” é permitido. Isso é trapaça!

Julie Lerman é uma Microsoft MVP, 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 ela é 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: Cesar de la Torre (Microsoft)