Arquitetura Orientada ao Domínio

A figura 1 mostra as peças base de uma arquitetura orientada ao domínio.

Ilustração 1:

Apenas como ilustração a figura apresenta o domínio e outros andares da aplicação. Uma arquitetura baseada em servlets é mostrada como ilustração mas podemos pensar que o objeto Action representado na figura se refere a um objeto abstrato que tanto pode ser um objeto inicializado por um servlet, tanto um componente de uma interface swing. O ponto é que o evento iniciado pelo usuário no andar de apresentação gera um evento, que de alguma forma, leva à execução de uma ação.

Ações de Leitura

Esta ação pode ser de dois tipos : leitura ou edição. As ações de leitura são passivas no sentido que não alteram o estado do sistema. Basicamente resumem-se a uma pesquisa no estado do sistema para poder mostrar algum tipo de informação ao usuário.Exemplos deste tipo de ação são a listagem de itens em uma lista ou a execução de um relatório. Neste tipo de ações estamos interessados em ler dados do estado do sistema. Muitas vezes o desafio aqui é fazer essa leitura o mais economicamente possível, ou seja, criando o mínimo de objetos possível e ser o mais rápido possível na execução. Padrão como Fastlane podem ajudar bastante a diminuir a carga sobre a aplicação.

Estas ações estão interessadas em consultar o estado do sistema. Em sistema orientado ao domínio isso significa consultar um ou mais repositórios de entidades, já que – nesta filosofia – é o conjunto de todas as instâncias de todas as entidades que forma o estado do sistema. Isto é representado na figura com uma seta apontando da ação para o repositório. Esta estratégia permite que o repositório seja informado com dicas para a extração dos dados priorizando assim os interesses de leitura da ação ( ou seja, maximizando a eficiência de leitura )

Ações de Edição

Este tipo de ações alteram o estado do sistema. Por causa disso estas ações são, freqüentemente, efetuadas em um ambiente transacional. Aqui o ponto não é ser rápido, é ter a certeza que o estado foi alterado de forma consistente. Vários passos são seguidos para ter o máximo de certeza à priori de que tudo vai acontecer com previsto e o sistema terá seu estado alterado corretamente. Estas verificações preemptivas são normalmente chamadas: validações. contudo esse termo causa grandes problemas de entendimento pelo que prefiro usar a expressão ‘verificação de constrangimento”. Para que a ação possa ser sequer tomada como candidata a poder alterar o estado do sistema todos os constrangimentos têm que ser verificados e a falha de algum deles inibe qualquer tentativa de alteração do estado do sistema.

A comunidade de desenvolvimento normalmente associa “edição” como “cadastro”. Na realidade isto atrapalha mais que ajuda. O ponto é que todos os sistema são dotados de ações de edição que alteram o estado do sistema. Seja esse estado traduzido como o conjunto de valores das propriedades de uma entidade, ou seja traduzido nos valores das propriedades de várias entidades relacionadas, isso é na realidade uma separação artificial daquilo que um software é e faz. Portanto, não se pense que com “acção de edição” estou me referindo a “edição de cadastros”. Não. Estou-me referindo a toda e qualquer ação que altere o estado do sistema.

Porque, como já mencionei, o estado do sistema é identificado com o estado das entidades do domínio em um sistema orientado ao domínio, então as ações de edição têm que atuar inevitávelmente nas entidades do domínio. Contudo as ações não sabem quais entidades têm que ser alteradas, nem quais dados devem ser usados na alteração. Essa responsabilidade é dos objetivos de Serviço. Estes objetos são responsáveis pela alteração do estado do sistema. Eles orquestram as entidades, seus estados e comportamentos para alterar o estado do sistema de alguma forma. Em particular formas de alteração diferentes são delegadas a serviços diferentes de forma conforme o Principio de Separação de Responsabilidade.

É possível, e até certo ponto, desejável, que um serviço coordene a atuação de outros serviços. Este serviço “coreógrafo” é na realidade um serviço implementando o padrão Façade. Este serviço coreógrafo é necessário para respeitar o Principio de Separação de Responsabilidade. Imagine por um momento que a ação necessita invocar o trabalho de vários serviços para completar uma alteração do estado do sistema. Ela estaria , de fato, orquestrando os serviços do domínio, chamando para si a responsabilidade de saber quais serviços chamar, em que ordem e com que parâmetros tornando a ação em um objeto do domínio. Isto seria um absurdo já que, por definição, a ação não é um objeto do domínio. Então, concluímos que para manter a separação de responsabilidade a ação apenas pode invocar um serviço do domínio. Esse serviço poderá invocar quantos outros serviços quiser.
O serviço “coreógrafo” ( Façade Service) é apenas uma construção de importância teórica para distinguir dois tipos de serviço: aquele que muda o estado do sistema, e aquele que delega essa alteração a mais do um outro serviço. Estas necessidade de composição de serviços é recorrente, mesmo quando os serviços não sõ do domínio, por exemplo: quando acontece a alteração de um dado no sistema um e-mail deve ser enviado informado o administrador. Esta necessidade de casar um serviço que muda o dado, com um serviço que envia o e-mail e tudo isso ainda dentro de um processo transacional tem que ser responsabilidade de um terceiro serviço “coreógrafo”.

Como nota final sobra dizer que a ação de leitura também pode invocar um serviço em vez do repositório. Embora isso significa apenas que o serviço irá delegar para o repositório e portanto criando uma interferência (aparentemente) inútil do serviço. A escolha sobre a visibilidade do repositório pertence ao desenvolvedor. Se ele for precavido ele usará um serviço de fachada ou , com um truque mais sutil um repositório de fachada. Um desenvolvedor aventureiro pensará apenas no seu trabalho e não deixará a estrutura pronta para uma fácil extensão futura. É um daquele dilemas que o desenvolvedor tem que resolver e que são o desafio da profissão…

Montagem

Compreendido que a ação apenas pode tomar dois caminhos e invocar ora um serviço, ora um repositório é comum que a ação seja responsável por diminuir a impedância entre as fronteiras do domínio e da aplicação passando a eles objetos também do domínio ( ou pelo menos, que pareçam objetos do domínio). Alguns destes objetos são complexos e é comum que seja necessário que eles tenham um relação direta com as entidades do domínio.

Embora, tecnicamente , a instância de uma classe só é um objeto do domínio quando está sob o controle de um repositório é comum necessitar da mesma estrutura de dados em outros andares da aplicação. O exemplo comum é usá-los na Apresentação.

A tentação de usar objetos da mesma classe de entidade é grande e, na prática, é a tendência atual. Ainda para mais em um sistema orientado ao domínio, isso faz todo o sentido. Assim, são criados instâncias das classes de entidade dentro da ação. Essas instâncias têm suas propriedades preenchidas pela ação e o resultado é enviado ao serviço. O serviço entende esse objeto como um objeto do domínio e processa a lógica subjacente ignorando que, na realidade, esse não é um objeto do domínio (já que não está sob a responsabilidade de um repositório).

Isto causa problemas ao repositório. Sendo que o objeto que lhe foi passado não está no domínio ( não é controlado pelo repositório), o repositórioassume que se trata de uma nova instância de uma entidade e tenta presevá-la atribuindo-lhe uma identidade. Esta identidade é inferida do objeto e isso pode levar a erros. Como não ha certeza da identidade daquela instância, o repositório não pode fazer muito mais que isso. Ele sempre vai interpretar a chegada da instância como uma adição ao estado atual e não uma alteração do estado atual.

Para tentar colmatar o problema temos a figura do Assembler (Montador). Este objeto é um delegado do domínio em outros andares. Ele sabe como montar a instância a partir de objetos de não-dominio (como mapas, por exemplo) e identificar a identidade do objeto se ele estiver presente nesses objetos que lhe são passados. Ele sabe também como, dado uma instância do domínio, desmembrá-la em objetos de não-domínio, atuando como um DesAssembler (“Des-Montador”)

O Repositório

O repositório tem como objetivo ser o localizador de instâncias das entidades que compõem o estado do sistema. Para o resto da aplicação é indiferente onde essas instâncias realmente estão. Se elas jazem num banco de dados, na memória, um uma nuvem de processamento, um arquivo xml , um sistema de prevalência ou simplesmente codificadas num feixe de luz, não importa. Mas para o repositório importa. Afinal ele é responsável por manter esses dados consistentes e várias coisas podem dar errado nesse trabalho.

Uma outra responsabilidade do repositório é prover mecanismos para que uma instância de uma entidade encontra uma outra instância de uma outra entidade (ou da mesma) com a qual está relacionada. Afinal a instância pode ser um grafo de objetos – o que seria otimo – mas a constante alteração dessas instâncias pelos serviços torna o grafo uma estrutura estressante para o domínio. Contudo, essa estrutura pode ser simulada com a ajuda do repositório. Afinal, para que uma instancia encontre a outra basta apenas saber a identidade dessa outra instância ou alguma regra que relaciona as duas. A procura em si, o repositorio executa.

O repositório é livre de preservar as instâncias como e onde quiser, contudo, hoje em dia, a facilidade oferecida pelo padrão DomainStore torna tudo muito mais simples.Uma implementação de DomainStore pretende oferecer preservação do estado dos objetos do domínio que formam o estado do sistema da forma mais transparente possível. Poderíamos argumentar se as implementações atuais entregam essa promessa, mas o ponto é que sempre será necessário escolher uma implementação especifica desse padrão; mesmo que seja uma implementação feita por você.

É esta a responsabilidade do Repositório, escolher e comunicar com um DomainStore e usá-lo para prover as informações pedidas pelos serviços (e/ou ações).

Critérios de Pesquisa

Uma das funcionalidades mais importantes em um sistema de informação com estado é poder pesquisar informações desse estado. Em sistemas orientados ao domínio isso significa pesquisar por instâncias de entidades que respeitem certas regras. Que respeitem certos critérios de pesquisa.

Estes critérios são, eles mesmos, objetos. Objetos normalmente implementando o padrão QueryObject. Contudo estes objetos são normalmente complexos de criar por conterem muitas propriedades e variações possiveis. É comum necessitar de um objeto Builder para ajudar na sua criação.

Existem dois usos para os critérios de pesquisa. a) o serviço montar um critério para procurar algo no repositório; b) o repositório monta um critério para procurar algo no DomainStore ( supondo que ele aceita este padrão como input). No primeiro caso queremos que o builder do critério seja o mais simples possível e que, se possivel, utilize os conceitos do domínio. Aqui normalmente somos tentados a dotar esse builder de uma interface fluente para facilitar a escrita e deixar tudo mais coerente com o propósito do domínio. No segundo caso o repositorio tem que consumir o builder disponibilizado pela API do DomainStore que está utilizando (se uma existir). Se o desenvolvedor escolher usar critérios de pesquisas nos dois casos, o repositorio será obrigado a traduzir um critério para o outro. Se isso acontecer, ele deverá delegar essa tarefa a um objeto que implemente o padrão Interpreter para que um critério seja “compilado” em outro. O importante é que, do ponto de vista do serviço não existe nenhum outro critério alem daquele que ele cria.

Como nota é importante chamar a atenção para o caso em que o desenvolvedor escolhe usar o builder e o critério definido pela API do DomainStore como critério e builder a ser usado pelo serviço. Isso destrói todo o objetivo da arquitetura orientada ao domínio pois agora o serviço não mais estará fazendo pesquisas sobre instâncias no repositório, mas sob objetos de dados controlados pelo DomainStore. Isto é uma quebra de encasuplamento e viola diretamente o Principio de Separação de Responsabilidade. Evite isso.

Licença

Creative Commons License Sérgio Taborda
Este trabalho é licenciado sob a
Licença Creative Commons Atribuição-Uso Não-Comercial-Não a obras derivadas 3.0 Genérica .

11 opiniões sobre “Arquitetura Orientada ao Domínio”

  1. Parabéns pelo artigo!
    Recentemente acabei de ler o livro de DDD do Eric Evans, e sabe como é né, sempre ficam algumas dúvidas ou mesmo detalhes a discutir.

    – Apesar de você não citar, eu acredito que você use o conceito de Value Object do DDD, por exemplo, a entidade Produto pode ter um atributo tipo, que este pode ser representado por um VO ProdutoTipo. Numa tela de cadastro do Produto, vamos supor que seja necessário carregar um combo com todos os Tipos de Produto. Agora vem a pergunta, eu pego Todos os Tipos de Produto do ProdutoRepositorio (a entidade forte no caso) ou eu crio um TipoProdutoRepositorio ou devo pegar estes VOs de um ProdutoServico ?

    – Ainda no caso dos VOs … eu preciso permitir que o usuário cadastre os Tipos de Produtos, o que fazer neste caso ? (Mesmo dilema da pergunta acima)

    – O papel do Assembler no diagrama “daoless” não está fazendo o trabalho da Factory de Entidade ? Não faz sentido criar metodos “assembler” no Repositorio que este repassa para Factory ?

    – Esta é a última eu prometo, :D. Como fica parte de controle de transação nos Repositorios ? Partindo da idéia que o repositorio “emula” uma collection em memoria, para salvar as entidades, como posso fazer ? crio um método add(Entidade) e no final executo um save ? O commit é feito no Serviço ?

    Valeu!
    Roger Leite

  2. Se vc permite que o usuário cadastre tipos de produto então isso significa que o estado do sistema vai mudar conforme esse cadastro ( mais tipos, mais possibilidades) . Isso significa portanto que o tipo do produto não é um VO e sim uma entidade. Logo, trate-o como uma entidade. Pegue os tipos do repositório deles.
    O assembler pode usar a factory mas o seu trabalho não é criar o objeto é preencher as propriedades do objeto com informação que vem de outro lugar da aplicação ( da UI , por exemplo). Por exemplo, ele pode pegar um Map obtido de request.getParametersMap() e preencher os campos de uma entidade Produto fazendo algo como produto.setXPTO( map.get(“XPTO”)).
    A transação é um processo controlado fora do repositório, fora até do dominio. A transação pode ser iniciada pelo serviço ou por algum mecanismo automático que encapsule a chamada ao serviço (padrão Proxy) como se faz em EJB. O ponto é que nem o repositorio nem o resto dos objetos do dominio – com exceção , talvez, do serviço – não sabem sequer o que é uma transação ou se estão em uma.

  3. Olá Sérgio, alguma coisas não ficaram claras para mim ai vão elas:

    Porque o service tem que acessar o repositório, não poderia acessar diretamente o DomainStore? Acho até errado o service acessar o repositório porque assim o repositorio é obrigado a ter metodos de edição que mudem o estado do sistema como :

    public void add(T object){
    session.save(object); //delegando pro domain store
    }
    public void remove(T object){
    session.delete(object);
    }

    //Este metodo nao deveria estar aqui
    public void update(T object){
    session.update(object);
    }

    O correto não seria deixar so as pesquisas no repositorio, além das operações de add e remove eh deixar as operaçãoes de update para o service que acessaria o DomainStore direto.

    O unico incomodo que vejo nisso eh que action teria que ter as duas classes por composição ou seja digamos uma ActionUsuario teria que ter

    public class ActionUser {
    private UserRepository ur;
    private UserService us;
    }

    Uma ultima duvida a função do assembler seria algo parecido com isso:

    Digamos que tenha um cadastro de usuario em um form html com nome elogin e apos enviar a requisição e chegar na action, a propria teria o asssembler que faria este trabalho:

    public class ActionUser {
    private Assembler assembler; //Cada classe teria seu montador, ou seria um montador só para todas as classes?
    private UserService us;

    public void createUser(){
    Usuario usuario=assembler.createUser(request.getParameterMap());
    us.save(usuario);

    }
    }

    Desculpa o excesso de dúvidas.

    Obrigado, seu blog tem me ajudado muito a arquiteturar melhor meus sistemas.

    Ou é correto o

  4. Primeiro que tudo ha que entender que o repositorio é primeiramente um objeto de procura. Nele estão encapsuladas as logicas (queries) para a procura. É ele que sabe encontrar as instâncias importantes paras os processos de negocio.
    O dominaStore sabe encontrar instancias com base em uma pesquisa genérica.
    Em comparação o DomainStore equivale a eu poder usar SQL. O SQL é genérico e qualquer sistema o pode usar. Mas para cada sistema eu tenho frases especificas que quero usar. Esse é o papel do Repositorio, definir e utilizar essas frases.
    O repositorio ser ou não editável é uma questão de gosto. Só tem um pequeno senão. Quando o seu processo de edição não for um simples save o domainstore não aguenta sozinho. Ai vc terá que colocar a logica no service… logica de persistencia no service ? = gambe.
    Logicas especiais são necessários quando vc faz uso de value objects como money, por exemplo. Vc tem que saber onde guarda o amount e o currency e de onde os recuperar depois. Nem sempre é do banco. O currencu, por exemplo, pode ser definido num properties.
    O ponto é que o repositorio é tb um ponto de extensão do dominio. Ele faz parte do dominio para isso mesmo. O domainStore não faz parte do dominio, ele apenas guarda o dominio.

    A action deve invocar o service. Sempre!

    public void createUser(){
    Usuario usuario=assembler.createUser(request.getParameterMap());

    try{
    UsuarioService uservice = Services.getUsuarioService();
    uservice.create (usuario);
    } catch (UserServiceException e){
    // trata exception. por exemplo, redirecionando.
    }
    }

    }

    public classe SimpleUserService implements UsuarioService{

    @Transactable
    public void create (Usuario user){
    repositorio.save(user);
    }

    }

    public class EmailSendingUsuerService implements UsuarioService{

    @Inject
    UsuarioRepository repositorio;
    @Inject
    EmailSenderService senderService;

    public void create (Usuario user){

    // gera password aleatoriamente
    user.setPassword(password);

    // guarda
    repositorio.save(user);

    Email email = …

    // monta email avisando o usuário que foi cadastrado e a sua passaword

    senderService.send(email);

    }

    }

    Outras implementações seriam possiveis, a imaginação/ requisistos são o limite.

    O ponto é que vc tem que dar a chance ao dominio de interferir com o fluxo das ações. É para isso que ele existe. Se a action faz tudo, não ha como o dominio interferir.

    A action é apenas uma das possiveis forma do dominio ser invocado. Por exemplo, poderiamos ter um webservice invocando o serviço ou o serviço correndo em um ESB. Se o serviço está desacoplado é facil integrá-lo com outras tecnologias. Se tudo está escrito no action, isso simplesmente dá muito trabalho. O action não pode executar logicas de negocio. Ele pode carregar dados e até fazer pesquisas , mas nada que altere o estado do sistema.

  5. Obrigado Sérgio novamente pelos esclarecimentos, o que acho estranho eh que o Service na maioria das vezes funcionara como um delegate apenas, tendo que duplicar metodos ou seja um metodo no Service e outro igual no Repository, mas compreendi que o Service tem que acessar o Repository e não o DomainStore direto, porque para alterar um Usuário tenho que antes fazer uma pesquisa para encontrá-lo, por isso o Service tem que usar o Repositorio porque as frases ja estao montadas nele pronta para achar o Usuario.
    Um outro ponto no caso do Montador em código falando era poderia ser traduzido para isto:

    public class Assembler(){

    public User CreateUser(Map request){
    User user=new User(request.getParameter(lnome),(request.getParameter(login));

    }
    }

    Isso não seria uma Factory? O colega acima tinha a mesma duvida, ai voce disse que o papel do montador era colocar as “coisas” no lugar, eh que ele poderia usar uma factory, mas colocar as “coisas” no objeto que vem da UI por exemplo não seria criar ele? Ainda não ficou claro qual seria a diferença entre a Factory e o Assembler.

    Sendo assim o Assembler seria unico para todos no sistema ? tipo com metodo statics para cada classe do sistema, ou cada classse teria seu Assembler tipo AssemblerUsuario, AssemblerPedido, etc.

    Novamente obrigado!

  6. Primeiro o service não pode ser um delegate porque um delegate delega para um service. Delegate é usado em sistemas distribuidos onde o serviço realfica centralizado. O Delegate delega a operação para o serviço real através da rede.

    O serviço não está delegando ao repositório. Ele está usando o repositório.
    Operações de validação, por exemplo, têm que ser feitas antes do save no repositorio. Chamada a outros serviços , etc… o caso em que o serviço simplesmente chama o repositorio é incomum. Mas mesmo nesse caso não ha nada de errado já que o serviço pode eveoluir nas suas reponsabilidades.

    Factory é um objeto responsável pela criação do objeto. Ele substitui a operação new e a logica do construtor. O objetivo é ter um objeto criado com estado válido.
    Assembler é um aroma de builder. Ele monta o objeto não no sentido que o cria (dá new) mas no sentido que o preenche. Ele obtém dados de outro lugar e preenche o objeto. Normalmente esse lugar são os escopos da aplicação (request, session, parameters). O Assembler substitui uma longa lista de chamadas a setters no objeto e get do request.
    O Assembler pode utilizar o Factory para dar o “new” no objeto mas depois irá invocar os setters (modificadores). O factory não invoca os setters para alterar o estado, como o assembler faz, ele invoca os setters para setar o primeiro estado possível para o objeto.Assembler é derivado de Builder , não de Factory. Deveriamos chamá-lo simplesmente de Builder, mas adotou-se o o nome Assembler para designar um builder especializado em montar o objeto a partir de dados que já existem em outro lugar (request, ejb, etc..)

    Em principio vc tem um assemble para cada entidade. Mas com reflection é muito simples criar um assembler que funciona para qualquer bean. A convenção bean ajuda muito na hora de usar reflection. Um assembler neutro que use apenas bean procura pelos sets , interpreta o nome do campo e puxar do request. No meio pode fazer alguma conversão de tipos já que o request só tem strings , ele pode converter para interios, booleanos, arrays etc.. sempre que se justificar. Em um assembler especifico de cada entidade vc faz essas conversões à mão porque já sabe que tipos está trabalhando. O BeanAssembler pode ser complexo, mas uma vez funcionando é uma mão na roda.

    Outra coisa. normalmente quando a entidade vai para a tela ela tem um id. Esse id é normalmente guardado num campo hidden e devolvido depois.
    Então o assembler irá também settar o id como se fosse um outro campo.
    Isso é importante para poder comparar o objeto criado pelo assembler, por exemplo para saber se se trata de uma edição ou de uma adição (adição o id é nulo, edição o id não é nulo)

  7. Olá, agora ficou tudo entendido sobre Assembler e Factory, achei muito interessante a ideia do BeanAssembler e vou tentar criar um.
    Um outro ponto quanto ao service eh o seguinte, no meu caso como uso um framework Web, as validações ficam por conta dele como campoRequerido, DataInválida, etc.. ou seja quando os dados chegam na action eu tenho certeza que estão válidos ai simplesmente eu chamo o service que chamo o Repositorio eh salva o Objeto. Neste caso onde uso framework, você não acha que o Service “perde” um pouco seu poder?

    Nesse sistema que estou construindo as regras de negocio permanecem todos no proprio Domain estou utilizando DomainDrivenDesign, você acha valida fazer a inversão

    HTTP > Servlet > Action > Domain > Repository > SGDB

    Visto que assim poderia ter metodos deste tipo :

    List lista=usuario.getPedidos();//Onde o Usuario tem um repositorio de Pedidos

    O que acha desta abordagem, a principio acho que quebra um pouco as camadas, mas vejo um q de interessante nela.

    Obrigado

  8. Hum… O framework valida a apresentação. Imagine que vc usa o seu serviço com uma apresentação de webservice. Quem faz a validação ? O serviço não pode assumir que o que ele recebe está correto. Mesmo quando já foi verificado pela camada superior. O ponto é que o serviço não sabe que existe uma camada superior que faz validações ( camadas inferirores não conhecem as superiores). Portanto, mesmo nesse cenário vc precisa de validação, nem que seja para lançar um BusinessException.

    Domain contém serviços e repositórios. Veja uma resposta anterior onde eu já falei isso.

    Não ha problema nenhum com usuario.getPedidos excepto que usuários não tem pedidos, clientes é que têm >; >

    List lista=usuario.asCustomer().getPedidos()

  9. Parabéns pelo artigo Sérgio. Muito interessante o Assembler, especialmente o BeanAssembler que você sugeriu em um dos comentários anteriores.

  10. Realmente esse artigo esta altamente bom mas também altamente técnico, apesar da Ilustração acima do diagrama de dominio geral, bem que poderia desmenbrar em outros diagramas UML para não ficar só na narrativa, ao menos ficaria menos oculta informações como(Padrões que não foram ilustrados) e de melhor exposição a visão.

    BOM,
    É só uma dica ; )

    Abraçosss

Deixe uma resposta para Daniel Bussade Cancelar resposta