Incompatibilidade conceitual entre DDD Application Services e API REST

20

Estou tentando criar um aplicativo que tenha um domínio comercial complexo e um requisito para oferecer suporte a uma API REST (não estritamente REST, mas orientada a recursos). Estou com problemas para encontrar uma maneira de expor o modelo de domínio de maneira orientada a recursos.

No DDD, os clientes de um modelo de domínio precisam passar pela camada processual de 'Application Services' para acessar qualquer funcionalidade de negócios, implementada por Entidades e Serviços de Domínio. Por exemplo, há um serviço de aplicativo com dois métodos para atualizar uma entidade de Usuário:

userService.ChangeName(name);
userService.ChangeEmail(email);

A API deste serviço de aplicativo expõe comandos (verbos, procedimentos), não estado.

Mas se também precisamos fornecer uma API RESTful para o mesmo aplicativo, existe um modelo de recursos do usuário, que se parece com isso:

{
name:"name",
email:"[email protected]"
}

A API orientada a recursos expõe estado , não comandos . Isso levanta as seguintes preocupações:

  • cada operação de atualização em uma API REST pode mapear para uma ou mais chamadas de procedimento do Serviço de Aplicativo, dependendo de quais propriedades estão sendo atualizadas no gabarito de recursos

  • cada operação de atualização parece atômica para o cliente da API REST, mas não é implementada dessa maneira. Cada chamada do Serviço de Aplicativo é projetada como uma transação separada. A atualização de um campo em um modelo de recursos pode alterar as regras de validação para outros campos. Portanto, precisamos validar todos os campos do modelo de recursos juntos para garantir que todas as chamadas em potencial do Serviço de Aplicativo sejam válidas antes de começarmos a realizá-las. Validar um conjunto de comandos de uma só vez é muito menos trivial do que executar um de cada vez. Como fazemos isso em um cliente que nem mesmo conhece comandos individuais?

  • chamar métodos de Serviço de Aplicativo em ordem diferente pode ter um efeito diferente, enquanto a API REST faz parecer que não há diferença (dentro de um recurso)

Eu poderia ter problemas mais semelhantes, mas basicamente todos são causados ​​pela mesma coisa. Após cada chamada para um Serviço de Aplicativo, o estado do sistema é alterado. Regras do que é mudança válida, o conjunto de ações que uma entidade pode executar na próxima mudança. Uma API orientada a recursos tenta fazer com que tudo pareça uma operação atômica. Mas a complexidade de atravessar essa lacuna deve ir a algum lugar, e parece enorme.

Além disso, se a interface do usuário for mais orientada a comandos, o que geralmente ocorre, teremos que mapear entre comandos e recursos no lado do cliente e depois no lado da API.

Questões:

  1. Toda essa complexidade deve ser tratada por uma camada de mapeamento (espessa) de REST para AppService?
  2. Ou estou faltando alguma coisa no meu entendimento de DDD / REST?
  3. O REST poderia simplesmente não ser prático para expor a funcionalidade de modelos de domínio em um certo grau (bastante baixo) de complexidade?
astreltsov
fonte
3
Pessoalmente, não considero o REST tão necessário. No entanto, é possível calçar o DDD nele: infoq.com/articles/rest-api-on-cqrs programmers.stackexchange.com/questions/242884/… blog.42.nl/articles/rest-and-ddd-incompatible
Den
Pense no cliente REST como um usuário do sistema. Eles não se importam absolutamente com COMO o sistema executa as ações que executa. Você não esperaria mais que o cliente REST conhecesse todas as ações diferentes no domínio do que esperaria que um usuário. Como você diz, essa lógica precisa ir a algum lugar, mas teria que ir a algum lugar em qualquer sistema, se você não estivesse usando o REST, seria apenas movê-la para o cliente. Não fazer isso é precisamente o objetivo do REST, o cliente deve saber apenas que deseja atualizar o estado e não deve ter idéia de como fazer isso.
Cormac Mulhall
2
@astr A resposta simples é que os recursos não são o seu modelo; portanto, o design do código de manipulação de recursos não deve afetar o design do seu modelo. Os recursos são um aspecto externo do sistema, onde o modelo é interno. Pense nos recursos da mesma maneira que você pensa na interface do usuário. Um usuário pode clicar em um único botão na interface do usuário e centenas de coisas diferentes acontecem no modelo. Semelhante a um recurso. Um cliente atualiza um recurso (uma única instrução PUT) e um milhão de coisas diferentes podem acontecer no modelo. É um anti-padrão acoplar seu modelo aos recursos.
Cormac Mulhall
1
É uma boa conversa sobre o tratamento de ações em seu domínio como efeitos colaterais das alterações de estado REST, mantendo seu domínio e a Web separados (avanço rápido de 25 minutos por pouco) yow.eventer.com/events/1004/talks/1047
Cormac Mulhall
1
Também não tenho certeza sobre toda a coisa "usuário como robô / máquina de estado". Eu acho que devemos nos esforçar para tornar nossas interfaces de usuário muito mais natural do que isso ...
guillaume31

Respostas:

10

Eu tive o mesmo problema e o "resolvi" modelando os recursos REST de maneira diferente, por exemplo:

/users/1  (contains basic user attributes) 
/users/1/email 
/users/1/activation 
/users/1/address

Então, eu basicamente dividi o recurso maior e complexo em vários outros menores. Cada um deles contém um grupo um tanto coeso de atributos do recurso original que se espera que sejam processados ​​juntos.

Cada operação nesses recursos é atômica, embora possa ser implementada usando vários métodos de serviço - pelo menos no Spring / Java EE, não é um problema criar transações maiores a partir de vários métodos que originalmente pretendiam ter sua própria transação (usando a transação REQUIRED propagação). Muitas vezes, você ainda precisa fazer uma validação extra para esse recurso especial, mas ainda é bastante gerenciável, pois os atributos são (supostamente) coesivos.

Isso também é bom para a abordagem HATEOAS, porque seus recursos mais refinados transmitem mais informações sobre o que você pode fazer com eles (em vez de ter essa lógica no cliente e no servidor porque não pode ser facilmente representada nos recursos).

Naturalmente, não é perfeito - se as UIs não forem modeladas com esses recursos em mente (especialmente as orientadas a dados), isso poderá criar alguns problemas - por exemplo, a UI apresenta grande forma de todos os atributos de determinados recursos (e seus sub-recursos) e permite que você edite-os todos e salve-os de uma só vez - isso cria uma ilusão de atomicidade, mesmo que o cliente precise chamar várias operações de recursos (que são atômicas, mas a sequência inteira não é atômica).

Além disso, essa divisão de recursos às vezes não é fácil ou óbvia. Faço isso principalmente em recursos com comportamentos complexos / ciclos de vida para gerenciar sua complexidade.

qbd
fonte
É o que eu tenho pensado também - crie representações de recursos mais granulares porque são mais convenientes para operações de gravação. Como você lida com a consulta de recursos quando eles se tornam tão granulares? Criar representações não normalizadas somente leitura?
Astreltsov
1
Não, não tenho representações des normalizadas somente leitura. Eu uso o padrão jsonapi.org e ele possui um mecanismo para incluir recursos relacionados na resposta para determinado recurso. Basicamente, digo "dê-me usuário com o ID 1 e também inclua seu email de sub-recursos e ativação". Isso ajuda a se livrar de chamadas REST extras para sub-recursos e não afeta a complexidade do cliente que lida com os sub-recursos se você usar alguma boa biblioteca de cliente da API JSON.
QbD
Portanto, uma única solicitação GET no servidor se traduz em uma ou mais consultas reais (dependendo de quantos sub-recursos estão incluídos) que são combinadas em um único objeto de recurso?
Astreltsov 26/04
E se for necessário mais de um nível de aninhamento?
Astreltsov
Sim, no dbs relacional, isso provavelmente será traduzido para várias consultas. O aninhamento arbitrário é suportado pela API JSON, é descrito aqui: jsonapi.org/format/#fetching-includes
qbd
0

O principal problema aqui é: como a lógica de negócios é chamada de forma transparente quando uma chamada REST é feita? Esse é um problema que não é tratado diretamente pelo REST.

Resolvi isso criando minha própria camada de gerenciamento de dados em um provedor de persistência como o JPA. Usando um metamodelo com anotações personalizadas, podemos chamar a lógica de negócios apropriada quando o estado da entidade é alterado. Isso garante que, independentemente de como o estado da entidade mude, a lógica de negócios seja invocada. Mantém sua arquitetura SECA e também sua lógica de negócios em um só lugar.

Usando o exemplo acima, podemos chamar um método de lógica de negócios chamado validateName quando o campo de nome é alterado usando REST:

class User { 
      String name;
      String email;

      /**
       * This method will be transparently invoked when the value of name is changed
       * by REST.
       * The XorUpdate annotation becomes effective for PUT/POST actions
       */
      @XorPostChange
      public void validateName() {
        if(name == null) {
          throw new IllegalStateException("Name cannot be set as null");
        }
      }
    }

Com essa ferramenta à sua disposição, tudo o que você precisará fazer é anotar seus métodos de lógica de negócios adequadamente.

codedabbler
fonte
0

Estou com problemas para encontrar uma maneira de expor o modelo de domínio de maneira orientada a recursos.

Você não deve expor o modelo de domínio de maneira orientada a recursos. Você deve expor o aplicativo de maneira orientada a recursos.

se a interface do usuário for mais orientada a comandos, o que geralmente acontece, teremos que mapear entre comandos e recursos no lado do cliente e depois no API.

De maneira nenhuma - envie os comandos para os recursos do aplicativo que fazem interface com o modelo de domínio.

cada operação de atualização em uma API REST pode mapear para uma ou mais chamadas de procedimento do Serviço de Aplicativo, dependendo de quais propriedades estão sendo atualizadas no gabarito de recursos

Sim, embora exista uma maneira ligeiramente diferente de soletrar isso, que pode tornar as coisas mais simples; cada operação de atualização em uma API REST é mapeada para um processo que envia comandos para um ou mais agregados.

cada operação de atualização parece atômica para o cliente da API REST, mas não é implementada dessa maneira. Cada chamada do Serviço de Aplicativo é projetada como uma transação separada. A atualização de um campo em um modelo de recursos pode alterar as regras de validação para outros campos. Portanto, precisamos validar todos os campos do modelo de recursos juntos para garantir que todas as chamadas em potencial do Serviço de Aplicativo sejam válidas antes de começarmos a realizá-las. Validar um conjunto de comandos de uma só vez é muito menos trivial do que executar um de cada vez. Como fazemos isso em um cliente que nem mesmo conhece comandos individuais?

Você está perseguindo a cauda errada aqui.

Imagine: tire o REST da imagem completamente. Imagine, em vez disso, que você estava escrevendo uma interface da área de trabalho para este aplicativo. Vamos imaginar ainda mais que você tem realmente bons requisitos de design e está implementando uma interface do usuário baseada em tarefas. Portanto, o usuário obtém uma interface minimalista perfeitamente ajustada para a tarefa em que está trabalhando; o usuário especifica algumas entradas e depois pressiona o "VERBO!" botão.

O que acontece agora? Da perspectiva do usuário, esta é uma tarefa atômica única a ser realizada. Da perspectiva do domainModel, há vários comandos sendo executados por agregados, nos quais cada comando é executado em uma transação separada. Esses são completamente incompatíveis! Precisamos de algo no meio para preencher a lacuna!

O algo é "a aplicação".

No caminho feliz, o aplicativo recebe algum DTO e analisa esse objeto para obter uma mensagem que entende e usa os dados na mensagem para criar comandos bem formados para um ou mais agregados. O aplicativo garantirá que cada um dos comandos enviados para os agregados esteja bem formado (que é a camada anticorrupção em funcionamento) e carregará os agregados e salvará os agregados se a transação for concluída com êxito. O agregado decidirá por si próprio se o comando é válido, dado seu estado atual.

Resultados possíveis - todos os comandos são executados com êxito - a camada anticorrupção rejeita a mensagem - alguns dos comandos são executados com êxito, mas um dos agregados reclama e você tem uma contingência para mitigar.

Agora, imagine que você tenha esse aplicativo criado; como você interage com ele de uma maneira RESTful?

  1. O cliente começa com uma descrição hipermídia do seu estado atual (por exemplo: a interface do usuário baseada em tarefas), incluindo controles hipermídia.
  2. O cliente envia uma representação da tarefa (ou seja: o DTO) para o recurso.
  3. O recurso analisa a solicitação HTTP recebida, agarra a representação e a entrega ao aplicativo.
  4. O aplicativo executa a tarefa; do ponto de vista do recurso, essa é uma caixa preta com um dos seguintes resultados
    • o aplicativo atualizou com êxito todos os agregados: o recurso relata êxito ao cliente, direcionando-o para um novo estado do aplicativo
    • a camada anticorrupção rejeita a mensagem: o recurso relata um erro 4xx ao cliente (provavelmente Solicitação incorreta), possivelmente transmitindo uma descrição do problema encontrado.
    • o aplicativo atualiza alguns agregados: o recurso relata ao cliente que o comando foi aceito e direciona o cliente para um recurso que fornecerá uma representação do andamento do comando.

Aceito é a cópia comum de quando o aplicativo adia o processamento de uma mensagem até depois de responder ao cliente - normalmente usado ao aceitar um comando assíncrono. Mas também funciona bem para esse caso, onde uma operação que deveria ser atômica precisa ser mitigada.

Nesse idioma, o recurso representa a própria tarefa - você inicia uma nova instância da tarefa postando a representação apropriada no recurso da tarefa, e esse recurso faz interface com o aplicativo e o direciona para o próximo estado do aplicativo.

No , praticamente sempre que você está coordenando vários comandos, você quer pensar em termos de um processo (também conhecido como processo de negócios, também conhecido como saga).

Há uma incompatibilidade conceitual semelhante no modelo de leitura. Mais uma vez, considere a interface baseada em tarefas; se a tarefa exigir a modificação de vários agregados, a interface do usuário para preparar a tarefa provavelmente incluirá dados de vários agregados. Se o seu esquema de recursos for 1: 1 com agregados, será difícil organizar; em vez disso, forneça um recurso que retorne uma representação dos dados de vários agregados, juntamente com um controle hipermídia que mapeie a relação "iniciar tarefa" com o terminal da tarefa, conforme discutido acima.

Veja também: REST in Practice por Jim Webber.

VoiceOfUnreason
fonte
Se estamos projetando a API para interagir com nosso domínio de acordo com nossos casos de uso. Por que não projetar as coisas de tal maneira que o Sagas não seja necessário? Talvez esteja faltando alguma coisa, mas lendo sua resposta, acredito realmente que o REST não é uma boa combinação com o DDD e é melhor usar procedimentos remotos (RPC). DDD é centrado no comportamento, enquanto REST é centrado no verbo http. Por que não remover REST da imagem e expor o comportamento (comandos) na API? Afinal, provavelmente eles foram projetados para satisfazer cenários de casos de uso e os prob são transacionais. Qual é a vantagem do REST se possuirmos a interface do usuário?
iberodev 2/12