Como trabalhar com exceções no DDD

imagem

Como parte da recente conferência DotNext 2018 , ocorreu o BoF no Domain Driven Design. Abordou a questão do trabalho com exceções, que causou um acalorado debate, mas não recebeu uma discussão detalhada, pois não era o tema principal.

Além disso, estudando muitos recursos, variando de perguntas sobre o fluxo de pilha e terminando com cursos de arquitetura pagos, é possível observar que a comunidade de TI tem uma atitude ambígua em relação às exceções e como usá-las.

É mencionado com mais freqüência que, usando exceções, é fácil criar um encadeamento de execução que tenha semântica de operador , o que afeta adversamente a legibilidade do código.

Existem opiniões diferentes sobre a criação de seus próprios tipos de exceções ou o uso dos padrões fornecidos no .NET.

Alguém faz validação com exceções e alguém em qualquer lugar usa a mônada Result . É verdade que Resultado permite que você entenda pela assinatura do método se é possível executar com êxito ou não. Mas não é menos verdade que, em linguagens imperativas (que incluem C #), o uso difundido de Result leva a códigos pouco legíveis, cobertos por construções de linguagem, de modo que é difícil entender o script original.

Neste artigo, falarei sobre as práticas adotadas por nossa equipe (em resumo - usamos todas as abordagens e nenhuma delas é um dogma).

Falaremos sobre um aplicativo corporativo criado com base no ASP.NET MVC + WebAPI. O aplicativo é construído em arquitetura de cebola , se comunica com o banco de dados e o intermediário de mensagens. Ele usa o log estruturado na pilha ELK e o monitoramento é configurado usando o Grafana.

Veremos como trabalhar com exceções de três perspectivas:

  1. Regras gerais de exceção
  2. Exceções, erros e arquitetura de cebola
  3. Casos especiais para aplicativos da Web

Regras gerais de exceção


  1. Exceções e erros não são a mesma coisa. Para exceções, usamos exceções, para erros - Resultado.
  2. As exceções são apenas para situações excepcionais, que por definição não podem ser muitas. Portanto, quanto menos exceções, melhor.
  3. O tratamento de exceções deve ser o mais granular possível. Como Richter escreveu em seu trabalho monumental.
  4. Se o erro for entregue ao usuário em sua forma original - use Resultado.
  5. Uma exceção não deve deixar os limites do sistema em sua forma original. Isso não é fácil de usar e oferece ao invasor uma maneira de explorar ainda mais as possíveis fraquezas do sistema.
  6. Se a exceção lançada é tratada pelo nosso aplicativo, não usamos exceção, mas Resultado. A implementação de exceções será ocultada pelo operador goto e, quanto pior, mais distante o código de processamento do código de lançamento de exceção. O resultado declara explicitamente a possibilidade de um erro e permite apenas o processamento "linear".

Exceções, erros e arquitetura de cebola


Nas seções a seguir, consideraremos as responsabilidades e regras para lançar / manipular exceções / erros para as seguintes camadas:

  • Hosts de aplicativos
  • Infra-estrutura
  • Serviços de aplicação
  • Núcleo do domínio

Host do aplicativo


O que é responsável por

  • Raiz de composição , personalizando a operação de todo o aplicativo.
  • O limite da interação com o mundo exterior são usuários, outros serviços, lançamento agendado.

Como essas são responsabilidades bastante complexas, vale a pena se limitarem. Damos as demais responsabilidades às camadas internas.

Como lidar com erros do Result

Transmite para o mundo externo, convertendo para o formato apropriado (por exemplo, em resposta http).

Como o resultado é gerado

De jeito nenhum. Esta camada não contém lógica, portanto não há lugar para gerar erros.

Como lidar com exceções

  1. Oculta detalhes e converte para um formato adequado para envio ao mundo externo
  2. Entra.

Como lançar exceções

De maneira alguma, essa camada é a mais externa e não contém lógica - não há ninguém para lançar uma exceção a ela.

Infra-estrutura


O que é responsável por

  1. Adaptadores para portas , ou simplesmente implementar interfaces de domínio, fornecendo acesso à infraestrutura - serviços de terceiros, bancos de dados, diretório ativo, etc. Essa camada deve ser o mais estúpida possível e conter o mínimo de lógica possível.
  2. Se necessário, ele pode atuar como uma camada anticorrupção .

Como lidar com erros do Result

Não conheço os provedores de banco de dados e outros serviços em execução na mônada Resultado. No entanto, alguns serviços operam com códigos de retorno. Nesse caso, os converteremos para o formato de resultado exigido pela porta.

Como o resultado é gerado

Em geral, essa camada não contém lógica, o que significa que não gera erros. Mas se usado como uma camada anticorrupção, uma variedade de opções é possível. Por exemplo, analisando exceções de um serviço legado e convertendo em Result aquelas exceções que são simples mensagens de validação.

Como lidar com exceções

No caso geral, ele lança ainda mais, se necessário, tendo garantido os detalhes. Se a porta que está sendo implementada permitir o retorno de Result no contrato, a infraestrutura converterá em Result os tipos de exceções que podem ser processadas.

Por exemplo, o intermediário de mensagens usado no projeto lança uma exceção ao tentar enviar uma mensagem quando o intermediário não está disponível. A camada de Serviços de Aplicativo está pronta para essa situação e pode lidar com ela com uma política de Nova Tentativa, Disjuntor ou reversão manual de dados.

Nesse caso, a camada de Serviços de Aplicativo declara um contrato que retorna Result em caso de erro. E a camada Infraestrutura implementa essa porta, convertendo a exceção do broker em Result. Naturalmente, ele converte apenas tipos específicos de exceções, e nem todos em uma linha.

Usando essa abordagem, temos duas vantagens:

  1. Declarar explicitamente a possibilidade de erros no contrato.
  2. Nos livramos da situação em que o Serviço de Aplicativo sabe como lidar com o erro, mas não sabe o tipo de exceção, pois é abstraído de um intermediário de mensagens específico. Criar um bloco catch no System.Exception base significa capturar todos os tipos de exceções, e não apenas aquelas que o Serviço de Aplicativo pode manipular.

Como lançar exceções

Depende das especificidades do sistema.

Por exemplo, as instruções Single e First LINQ lançam uma InvalidOperationException ao solicitar dados inexistentes. Mas esse tipo de exceção é usado em qualquer lugar do .NET, o que torna impossível processá-lo granularmente.

Na equipe, adotamos a prática de criar um ItemNotFoundException personalizado e lançá-lo da camada de infraestrutura se os dados solicitados não foram encontrados e não deveriam ser assim, de acordo com as regras de negócios.

Se os dados solicitados não forem encontrados e isso for permitido, eles deverão ser declarados explicitamente no contrato do porto. Por exemplo, usando a mônada Maybe .

Serviços de aplicação


O que é responsável por

  1. Validação de dados de entrada.
  2. Orquestração e coordenação de serviços - início e fim de transações, implementação de scripts distribuídos, etc.
  3. Faça o download de objetos de domínio e dados externos via portas para Infraestrutura, subseqüente chamada de comandos no Núcleo do Domínio.

Como lidar com erros do Result

Os erros do núcleo do domínio se traduzem no mundo exterior inalterados. Os erros da infraestrutura podem ser tratados por meio de novas tentativas, políticas do disjuntor ou difusão para o exterior.

Como o resultado é gerado

Pode implementar a validação como resultado.

Pode gerar notificações de sucesso parcial da operação. Por exemplo, mensagens para um usuário como “Seu pedido foi feito com sucesso, mas ocorreu um erro ao verificar o endereço de entrega. Um especialista entrará em contato com você em breve para esclarecer os detalhes da entrega. ”

Como lidar com exceções

Supondo que as exceções de infraestrutura que o aplicativo possa manipular já sejam convertidas pela camada Infraestrutura em Resultado, ele não trata disso.

Como lançar exceções

Em geral, de jeito nenhum. Mas existem opções limítrofes descritas na seção final do artigo.

Núcleo do domínio


O que é responsável por

A implementação da lógica de negócios, o "núcleo" do sistema e o principal significado de sua existência.

Como lidar com erros do Result

Como a camada é interna e os erros são possíveis apenas a partir de objetos no mesmo domínio, o processamento é reduzido às regras de negócios ou à conversão do erro para cima em sua forma original.

Como o resultado é gerado

Se você violar regras de negócios encapsuladas no Núcleo do Domínio e não cobertas pela validação dos dados de entrada no nível de Serviços de Aplicativo. Em geral, nessa camada, o resultado é usado com mais frequência.

Como lidar com exceções

De jeito nenhum. As exceções de infraestrutura já foram processadas pela camada Infraestrutura, os dados já chegaram estruturados, completos e validados graças à camada Serviços de Aplicativos. Consequentemente, todas as exceções que podem surgir serão verdadeiramente exceções.

Como lançar exceções

Normalmente, uma regra geral funciona aqui: quanto menos exceções, melhor.

Mas você já teve situações em que escreve código e entende que, sob certas condições, pode fazer negócios terríveis? Por exemplo, para anular o dinheiro duas vezes ou para estragar tanto os dados que não conseguimos coletar os ossos.

Como regra, estamos falando sobre a execução de comandos inaceitáveis ​​para o estado atual do objeto.

Obviamente, o botão correspondente na interface do usuário não deve estar visível nesse estado. Não devemos receber um comando do barramento neste estado. Tudo isso é verdade, desde que as camadas e sistemas externos desempenhem sua função normalmente . Mas no Domain Core não devemos saber sobre a existência de camadas externas e acreditar na correção de seu trabalho, devemos proteger os invariantes do sistema.

Algumas das verificações podem ser feitas no Application Services no nível de validação. Mas isso pode se transformar em programação defensiva , o que, em casos extremos, leva ao seguinte:

  1. O encapsulamento é enfraquecido, pois certos invariantes devem ser verificados na camada externa.
  2. O conhecimento da área de assunto “flui” para a camada externa; as verificações podem ser duplicadas por ambas as camadas.
  3. Validar a execução de um comando de uma camada externa pode ser mais complexo e menos confiável do que verificar se um objeto de domínio não pode executar um comando em seu estado atual.

Além disso, se colocarmos essas verificações na camada de validação, devemos informar ao usuário o motivo do erro. Dado que estamos falando de uma operação que não pode ser executada nas condições atuais, corremos o risco de estar em uma das duas situações:

  • Enviamos a um usuário comum uma mensagem que ele não entendia e que iria oferecer suporte de qualquer maneira, assim como na mensagem "Ocorreu um erro inesperado".
  • Informamos o vilão de uma forma bastante inteligível por que ele não pode executar a operação que deseja executar e ele pode procurar outras soluções alternativas.

Mas voltando ao tópico principal do artigo. Por todas as indicações, a situação em discussão é excepcional. Isso nunca deve acontecer, mas se acontecer, será ruim.

É mais lógico nessa situação lançar uma exceção, prometer os detalhes necessários, retornar ao usuário um erro da forma geral “A operação não é viável”, configurar o monitoramento para esse tipo de erro e esperar que nunca os vejamos.

Que tipo ou tipos de exceções para usar neste caso? Logicamente, esse deve ser um tipo separado de exceção, para que possamos diferenciá-lo dos outros e para que não seja capturado acidentalmente pelo tratamento de exceções da camada externa. Também não precisamos de uma hierarquia ou muitas exceções, a essência é a mesma - algo inaceitável aconteceu. Em nossos projetos, criamos um tipo CorruptedInvariantException para isso e o usamos em situações apropriadas.

Casos especiais para aplicativos da Web


Uma diferença significativa entre aplicativos da Web de outros (serviços de desktop, daemons e windows etc.) é a interação com o mundo externo na forma de operações de curto prazo (processamento de solicitações HTTP), após o qual o aplicativo "esquece" imediatamente o que aconteceu.

Além disso, após o processamento da solicitação, sempre é gerada uma resposta. Se a operação executada pelo nosso código não retornar dados, a plataforma ainda retornará uma resposta contendo o código de status. Se a operação foi interrompida por uma exceção, a plataforma ainda retornará uma resposta contendo o código de status correspondente.

Para implementar esse comportamento, o processamento de solicitações em plataformas da Web é criado na forma de pipes. Primeiro, a solicitação é processada sequencialmente (solicitação) e, em seguida, a resposta é preparada.

Podemos usar middleware, filtro de ação, manipulador de http ou filtro ISAPI (dependendo da plataforma) e integrar esse pipeline a qualquer momento. E em qualquer estágio do processamento da solicitação, podemos interromper o processamento e o pipeline continuará formando uma resposta.

Como regra, não implementamos mais a parte de negócios do aplicativo na arquitetura de pipeline, mas escrevemos o código que executa operações sequencialmente. E com essa abordagem, é um pouco mais difícil implementar o cenário quando interrompemos a execução da solicitação e prosseguimos imediatamente para a formação da resposta.

O que tudo isso tem a ver com o tratamento de exceções, você pergunta?

O fato é que as regras para trabalhar com exceções descritas nas partes anteriores do artigo não se encaixam bem nesse cenário.

Exceções são ruins de usar porque é semântica.

O uso generalizado de Result leva ao fato de arrastá-lo (Result) por todas as camadas do aplicativo e, ao formar a resposta, precisamos analisar o resultado de alguma forma para entender qual código de status retornar. Também é aconselhável generalizar e enviar esse código de análise para o Middleware ou ActionFilter, que se torna uma aventura separada. Ou seja, o resultado não é muito melhor do que exceções.

O que fazer em tal situação?

Não construa um absoluto. Estabelecemos as regras para nosso próprio benefício, e não em detrimento.

Se você deseja abortar uma operação porque sua continuação é impossível, lançar uma exceção não terá goto semântica. Dirigimos a execução para a saída, e não para outro bloco de código comercial.

Se o motivo da interrupção for importante para determinar o código de status desejado, tipos de exceção personalizados poderão ser usados.

Anteriormente, mencionamos dois tipos personalizados que usamos: ItemNotFoundException (transformando em 404) e CorruptedInvariant (transformando em 500).

Se você verificar os direitos dos usuários, porque eles não se enquadram no modelo ou nas reivindicações s, é permitido criar uma ForbiddenException personalizada (código de status 403).

E, finalmente, validação. Ainda não podemos fazer nada até o usuário modificar sua solicitação. Essa semântica é descrita pelo código 422 . Então, interrompemos a operação e enviamos a solicitação diretamente para a saída. Isso também pode ser feito usando a exceção. Por exemplo, a biblioteca FluentValidation já possui um tipo de exceção interno que transmite ao cliente todos os detalhes necessários para exibir claramente ao usuário o que há de errado com a solicitação.

Só isso. Como você trabalha com exceções?

Source: https://habr.com/ru/post/pt431804/


All Articles