Posts Marcados DDD

Repositórios e Expressões Lambda

Esta semana fizemos mais uma grande melhoria na nossa camada de domínio, incorporando expressões lambda .Net nas nossas consultas e parâmetros de ordenação de repositórios. Retomando o post sobre a camada de domínio, os repositórios são utilizados para tirar a dependência da aplicação dos detalhes de persistência de objetos e para ser uma representação abstrata de uma coleção de entidades de domínio.

A idéia é que eles facilitem a busca de entidades específicas ou implementem métodos mais complexos para a execução de consultas. Como no nosso caso usamos o NHibernate para camada de persistência, nós temos duas opções de efetuar estas consultas, os Criterias (DetachedCriteria ou expressões ICriterion simples) ou o HQL, que é uma linguagem de consulta parecida com SQL só que composta pelas entidades de domínio.

Consultas HQL são baseadas em strings (ver exemplo abaixo do nosso sistema de ServiceDesk), o que é extremamente flexível porém não suporta intellisense nem é compilada (o que pode facilitar erros no caso de refactors). Mas, para o HQL não temos muita opção, a alternativa que seria usar o NH Linq ainda não é viável (pelo que eu tenho visto, ainda não suporta muitas coisas).


public int TotalInbox(User user) {
   const string hql =
       @"select count(*) from Incident i inner join i.Allocation a
         inner join a.SupportGroup s inner join s.Users u 
         inner join i.Company c
         where i.ClosedOn is null and a.SupportUser is null
         and i.State != isnull(c.WaitingUserConfirmationState,-1) 
         and i.State != isnull(c.WaitingUserInformationState, -1) 
         and u = ?";
   return (int) ExecuteScalar<long>(hql, user);
}

O uso de Criterias é mais simples e é indicado para consultas com restrições diretas na entidade base (ou nas que possuem relacionamento direto com a entidade base). Porém, nos criterias é necessário especificar o nome da propriedade como string. Novamente, sem intellisense e sujeito a problemas de refactor. Como o uso de Criterias é muito mais freqüente, nós automatizamos na nossa camada de domínio a geração de uma classe estática com todas as propriedades das entidades de domínio (usando arquivos .tt de um editor T4). Desta forma, temos intellisense e no caso de refactor que cause alguma quebra, teremos um erro de compilação (já que as classes são sempre regeradas). E, conforme falado no post da camada de domínio, construímos ainda uma fluent interface para facilitar a escrita. O exemplo abaixo mostra isto em funcionamento.

public long TotalClosed(User user) {
    return Query.Where(Restrictions.Eq(PN.User, user))
           .And(Restrictions.IsNotNull(PN.ClosedOn)).Count();
}

Esta solução estava bastante adequada até aparecer a alternativa de usarmos expressões lambda ao invés de gerar as classes estáticas. Existem algumas iniciativas já fazendo isto, como o projeto NHLambdaExtensions. Neste caso específico, porém, eles estão abordando isto como uma biblioteca à parte para a geração de Criterias do NH e não como algo integrado aos repositórios.

Felizmente, na nossa camada de domíno, os repositórios já são tipados para a entidade de domínio que eles representam. Desta foram, fica simples gerar as expressões. O exemplo abaixo mostra a mesma rotina acima, agora usando expressões lamdas.

public long TotalClosed(User user) {
    return 
      Query.Where(i => i.User == user && i.ClosedOn != null).Count();
}

Como pode ser visto, bem mais legível e intuitivo. E sem a necessidade de se usar classes estáticas de apoio! Ainda temos algum trabalho a ser feito, pois não é simples traduzir todos os tipos de lambda para restrições válidas. Mas para os casos mais simples como o acima, está já 100% funcionando.

Outro ponto interessante é que as funções que aceitam lambda podem ser expostos para camadas superiores de domínio, já que usar uma expressão lambda não causa dependência para a camada de persistência. Isto diminui de maneira significativa o próprio tamanho da implementação dos repositórios.

Finalmente, a utilização deste tipo de solução acaba sendo algo tão flexível que estamos passando a adotar em vários outros cenários. Na definição de classes de ordenação para consultas, na identificação de campos usados como atributos em formulários e controllers e assim por diante. Realmente algo muito simples que está aí desde o lançamento da versão 3.5 do .Net mas que demorou para  a “ficha cair” achar uma maneira legal de aplicar na nossa infraestrutura.

Quem quiser saber algum detalhe mais técnico da implementação basta me mandar um email. Até a próxima.

, , , , ,

Deixe um comentário

Produtividade – Camada de Domínio

Continuando a série sobre produtividade, vou falar neste post sobre a camada de domínio. A camada de domínio é a que contem todas as entidades e regras de negócio, sendo utilizada pela camada de interface e fazendo uso do banco de dados para armazenamento de informações.

Inicialmente existe a questão da impedância objeto-relacional. Para o desenvolvimento o usual é trabalhar com objetos em código para representar objetos do mundo real, com seus vários atributos e relacionamentos. Uma tabela de banco é uma representação mais pobre e difícil de manipular, por isto trabalhar com objetos é mais simples. No entanto, o banco de dados relacional é necessário (bancos de dados orientados a objeto ainda são praticamente inviáveis, por uma série de razões), logo, deve haver uma maneira de converter classes em tabelas e instâncias em registros. Assim, o uso de um ORM é fundamental, pois esta tarefa tem que ser simples, não faz sentido gastarmos tempo de desenvolvimento escrevendo e debugando comandos SQL básicos.

Nosso ORM de escolha é o nHibernate (NH). No entanto, escrever arquivos de configuração paro nHibernate é trabalhoso (e não queremos trocar o trabalho de escrever SQL para o de escrever configurações), assim utilizamos também o Castle ActiveRecord (AR). O AR permite definir objetos com simples atributos de decoração e ele já traz também uma série de implementações pra facilitar o uso do NH. Apesar de ser mais simples, decorar classes e atributos ainda é trabalhoso (e erros nesta etapa podem causar graves conseqüências para aplicação), então para simplificar ainda mais, utilizamos o ActiveWriter (AW), que é uma DSL gráfica que permite descrever as entidades e ele gera todas as classes e propriedades já decoradas. O AW tem outras vantagens, descritas a seguir.

Até aqui super simples, mas existem duas guerras religiosas envolvidas que acho importante citar. As duas têm relação com o chamado “modelo anêmico”, definido pelo Fowler. A primeira é sobre se todos os objetos de domínio devem ou não ter representação direta em banco. Em um modelo OO puro isto não seria desta forma. Porém a minha opinião é de que é muito mais simples, pra efeito de desenvolvimento, manutenção, extração e até conversa com os usuários avançados, que haja o mapeamento 1:1. Ou seja, cada classe é uma tabela, cada registro uma instância. Os puristas afirmam que isto não é OO, pois o modelo de entidades de negócio acaba sendo só uma representação do banco. Na minha visão sim, pode ser… Porém como o banco na verdade é criado para suportar as classes, há um caminho de duas vias aí. E os benefícios que tenho colhido nos últimos 10 anos trabalhando desta maneira são muito grandes pra tentar mudar simplesmente pelo purismo. Assim, no nosso caso, há o mapeamento 1:1 sempre.

A segunda guerra religiosa tem a ver como onde as regras de negócio são implementadas. No Domain-Driven Design (DDD), existe o conceito de serviços, porém muitos acham que regras específicas de objetos devem estar na classe. Eu não. Eu acho que TODAS as regras de negócio devem estar definidas em Serviços (com exceção de regras de consultas, que estão nos repositórios e de regras de construção que estão nas fábricas). Com isto o nosso modelo é por definição, completamente anêmico. Nossos objetos são somente POCO e todas as regras estão sempre localizadas nos serviços. Apesar de alguns acharem isto extremo, no meu caso eu tenho tido muito sucesso trabalhando desta forma. Quando se tenta colocar regras em objetos, muitas vezes há a dúvida de se uma regra pertence a uma ou outra classe. Com o tempo, ninguém sabe onde está o que. Além disto, fica muito mais difícil trabalhar com AOP desta forma, pois em caso de um objeto chamar outro, fica difícil definir de quem é a responsabilidade e controlar transações, segurança, erros etc. Da maneira como eu faço, que é tudo na camada de serviços, ninguém nunca tem duvida de onde procurar uma regra, todos os aspectos estão lá e a manutenção do sistema é super tranqüila. Tenho sistemas de mais de 10 anos (com objetos ainda em C++!) onde continua sendo fácil mesmo pra quem não conhece, de identificar e alterar regras de negócio.

Assim, na nossa implementação, temos um repositório e uma fábrica para cada entidade de negócio (que por sua vez representa uma tabela). E os serviços são criados conforme necessário, onde cada regra é stateless e completamente procedural. Puristas falam que isto não é DDD. Pode ser. Mas é muito produtivo!

Para gerar serviços e repositórios, utilizamos arquivos de template (.TT) do visual studio. Eles utilizam a definição gerada pelo AW e automaticamente já constroem todos os repositórios, fábricas, operadores e demais artefatos de auxílio. Ou seja, ao arrastar graficamente uma tabela para o nosso diagrama AW, todos os artefatos mínimos para uso da entidade já estão prontos! Claro que iremos expandir os repositórios e fábricas com novos métodos e isto é feito através de classes parciais. Todos os repositórios, fábricas e serviços são implementados através de um container IoC, o Castle Windsor. Com isto uma troca futura de qualquer destes componentes é super simples – p. ex., a única dependência para NH que temos são as implementações de repositórios. Tudo fica em um único assembly que chamamos de assembly de domínio.

Finalmente, temos algumas classes estáticas de apoio como, por exemplo, uma com todos os nomes de propriedades de todas as entidades, outra com um acesso rápido para repositórios e fábricas e uma com todos os serviços do projeto. Outra classe muito útil é a de geração de consultas. Em repositórios, as consultas podem ser feitas via HQL do NH ou via consulta direta. Como a maior parte das consultas é simples, elas são feitas por critérios diretos e pra isto utilizamos uma interface fluente para ajudar a escrever. Exemplo:

return Query.Where(Restrictions.Eq(PN.User, user)).And(Restrictions.IsNotNull(PN.ClosedOn)).Count();

A consulta é feita para uma entidade chamada “Incident” e retorna o total aberto para um usuário. Query é a classe de apoio de consulta e PN são os nomes de propriedades – usamos isto para evitar strings e com isto minimizar o número de erros de digitação.

Estou muito satisfeito com a qualidade e a produtividade gerada pela nossa camada atual de domínio. Fazer e dar manutenção em objetos é algo que para nós é super simples e podemos nos dedicar e escrever regras de negócio e interface. O próximo post vai ser sobre a camada de interface.

, , , , , , ,

2 Comentários