Cómo trabajar con excepciones en DDD

imagen

Como parte de la reciente conferencia DotNext 2018 , se llevó a cabo BoF on Domain Driven Design. Abordó la cuestión de trabajar con excepciones, lo que provocó un acalorado debate, pero no recibió una discusión detallada, ya que no era el tema principal.

Además, al estudiar una gran cantidad de recursos, desde preguntas sobre stackoverflow hasta cursos de arquitectura de pago, puede observar que la comunidad de TI tiene una actitud ambigua hacia las excepciones y cómo usarlas.

Se menciona con mayor frecuencia que, usando excepciones, es fácil construir un hilo de ejecución que tenga una semántica de operador de goto , lo que afecta negativamente la legibilidad del código.

Existen diferentes opiniones sobre si crear sus propios tipos de excepciones o utilizar las estándar proporcionadas en .NET.

Alguien valida las excepciones, y alguien en todas partes usa la mónada Result . Es cierto que Result le permite comprender mediante la firma del método si es posible o no una ejecución exitosa. Pero no es menos cierto que en los lenguajes imperativos (que incluyen C #), el uso generalizado de Result conduce a un código poco legible, cubierto con construcciones de lenguaje, por lo que es difícil distinguir el script original.

En este artículo hablaré sobre las prácticas adoptadas por nuestro equipo (en resumen, utilizamos todos los enfoques y ninguno de ellos es un dogma).

Hablaremos sobre una aplicación empresarial construida sobre la base de ASP.NET MVC + WebAPI. La aplicación está construida con arquitectura de cebolla , se comunica con la base de datos y el intermediario de mensajes. Utiliza el registro estructurado en la pila ELK y el monitoreo se configura con Grafana.

Consideraremos trabajar con excepciones desde tres perspectivas:

  1. Reglas de excepción general
  2. Excepciones, errores y arquitectura de cebolla
  3. Casos especiales para aplicaciones web

Reglas de excepción general


  1. Excepciones y errores no son lo mismo. Para excepciones usamos excepciones, para errores - Resultado.
  2. Las excepciones son solo para situaciones excepcionales, que por definición no pueden ser muchas. Entonces, cuantas menos excepciones, mejor.
  3. El manejo de excepciones debe ser lo más granular posible. Como Richter escribió en su monumental obra.
  4. Si el error se debe entregar al usuario en su forma original, use Resultado.
  5. Una excepción no debe dejar los límites del sistema en su forma original. Esto no es fácil de usar y le da al atacante una forma de explorar aún más las posibles debilidades del sistema.
  6. Si nuestra aplicación maneja la excepción lanzada, no usamos excepción, sino Resultado. La implementación de las excepciones estará oculta por el operador goto y, cuanto peor sea, más se alejará el código de procesamiento del código de excepción. El resultado declara explícitamente la posibilidad de un error y permite solo su procesamiento "lineal".

Excepciones, errores y arquitectura de cebolla


En las siguientes secciones, consideraremos las responsabilidades y reglas para lanzar / manejar excepciones / errores para las siguientes capas:

  • Hosts de aplicaciones
  • Infraestructura
  • Servicios de solicitud
  • Núcleo de dominio

Host de aplicaciones


¿De qué es responsable?

  • Composición raíz , personalizando el funcionamiento de toda la aplicación.
  • El límite de la interacción con el mundo exterior son los usuarios, otros servicios, el lanzamiento programado.

Dado que estas son responsabilidades bastante complejas, vale la pena limitarse. Damos las responsabilidades restantes a las capas internas.

Cómo manejar los errores del resultado

Transmite al mundo exterior, convirtiéndolo al formato apropiado (por ejemplo, en respuesta http).

Cómo se genera el resultado

De ninguna manera Esta capa no contiene lógica, por lo que no hay ningún lugar para generar errores.

Cómo manejar excepciones

  1. Oculta detalles y convierte a un formato adecuado para enviar al mundo exterior
  2. Inicia sesión

Cómo lanzar excepciones

De ninguna manera, esta capa es la más externa y no contiene lógica, no hay nadie para lanzarle una excepción.

Infraestructura


¿De qué es responsable?

  1. Adaptadores a puertos , o simplemente para implementar interfaces de dominio, que dan acceso a la infraestructura: servicios de terceros, bases de datos, directorio activo, etc. Esta capa debe ser lo más estúpida posible y contener la menor lógica posible.
  2. Si es necesario, puede actuar como una capa anticorrupción .

Cómo manejar los errores del resultado

No conozco los proveedores de bases de datos y otros servicios que se ejecutan en la mónada Result. Sin embargo, algunos servicios operan con códigos de retorno. En este caso, los convertiremos al formato de resultado requerido por el puerto.

Cómo se genera el resultado

En general, esta capa no contiene lógica, lo que significa que no genera errores. Pero si se usa como una capa anticorrupción, es posible una variedad de opciones. Por ejemplo, analizar excepciones de un servicio heredado y convertir a Result aquellas excepciones que son simples mensajes de validación.

Cómo manejar excepciones

En el caso general, lo arroja más lejos, si es necesario, después de haber asegurado los detalles. Si el puerto que se está implementando permite la devolución del Resultado en el contrato, la infraestructura convertirá en Resultado los tipos de excepciones que se pueden procesar.

Por ejemplo, el intermediario de mensajes utilizado en el proyecto genera una excepción al intentar enviar un mensaje cuando el intermediario no está disponible. La capa de Application Services está lista para esta situación y puede manejarla con una política de reintento, un disyuntor o una reversión manual de datos.

En este caso, la capa de Servicios de aplicaciones declara un contrato que devuelve Resultado en caso de error. Y la capa de Infraestructura implementa este puerto, convirtiendo la excepción del intermediario en Resultado. Naturalmente, solo convierte tipos específicos de excepciones, y no todas seguidas.

Usando este enfoque, obtenemos dos ventajas:

  1. Declarar explícitamente la posibilidad de errores en el contrato.
  2. Nos deshacemos de la situación cuando el Servicio de aplicaciones sabe cómo manejar el error, pero no conoce el tipo de excepción, ya que se abstrae de un agente de mensajes específico. Construir un bloque catch en la base System.Exception significa capturar todo tipo de excepciones, y no solo aquellas que el Servicio de aplicaciones puede manejar.

Cómo lanzar excepciones

Depende de los detalles del sistema.

Por ejemplo, las sentencias Single y First LINQ arrojan una InvalidOperationException cuando solicitan datos inexistentes. Pero este tipo de excepción se usa en todas partes en .NET, lo que hace que sea imposible procesarlo de forma granular.

En el equipo, adoptamos la práctica de crear una ItemNotFoundException personalizada y lanzarla desde la capa de infraestructura si no se encontraron los datos solicitados y no deberían serlo de acuerdo con las reglas comerciales.

Si no se encuentran los datos solicitados y esto es permisible, debe declararse explícitamente en el contrato del puerto. Por ejemplo, usando la mónada Quizás .

Servicios de solicitud


¿De qué es responsable?

  1. Validación de datos de entrada.
  2. Orquestación y coordinación de servicios: inicio y finalización de transacciones, implementación de scripts distribuidos, etc.
  3. Descargue objetos de dominio y datos externos a través de puertos a Infraestructura, llamada posterior de comandos en Domain Core.

Cómo manejar los errores del resultado

Los errores del núcleo del dominio se traducen en el mundo exterior sin cambios. Los errores de la infraestructura se pueden manejar mediante el reintento, las políticas de disyuntores o la transmisión al exterior.

Cómo se genera el resultado

Puede implementar la validación como resultado.

Puede generar notificaciones de éxito parcial de la operación. Por ejemplo, mensajes a un usuario como "Su pedido se ha realizado correctamente, pero se produjo un error al verificar la dirección de entrega. Un especialista se comunicará con usted en breve para aclarar los detalles de la entrega ".

Cómo manejar excepciones

Suponiendo que las excepciones de infraestructura que la aplicación puede manejar ya están convertidas por la capa de Infraestructura en Resultado, no lo maneja en absoluto.

Cómo lanzar excepciones

En general, de ninguna manera. Pero hay opciones límite descritas en la sección final del artículo.

Núcleo de dominio


¿De qué es responsable?

La implementación de la lógica de negocios, el "núcleo" del sistema y el significado principal de su existencia.

Cómo manejar los errores del resultado

Como la capa es interna y los errores solo son posibles a partir de objetos en el mismo dominio, el procesamiento se reduce a las reglas comerciales o a la traducción del error hacia arriba en su forma original.

Cómo se genera el resultado

Si viola las reglas comerciales que están encapsuladas en Domain Core y no están cubiertas por la validación de datos de entrada a nivel de Servicios de aplicaciones. En general, en esta capa, el resultado se usa con mayor frecuencia.

Cómo manejar excepciones

De ninguna manera Las excepciones de infraestructura ya han sido procesadas por la capa de Infraestructura, los datos ya han llegado estructurados, completos y validados gracias a la capa de Servicios de Aplicación. En consecuencia, todas las excepciones que pueden volar serán realmente excepciones.

Cómo lanzar excepciones

Por lo general, una regla general funciona aquí: cuantas menos excepciones, mejor.

Pero, ¿alguna vez ha tenido situaciones en las que escribe código y comprende que, bajo ciertas condiciones, puede hacer negocios terribles? Por ejemplo, para cancelar el dinero dos veces o estropear los datos tanto que no podamos recolectar los huesos.

Como regla, estamos hablando de ejecutar comandos que son inaceptables para el estado actual del objeto.

Por supuesto, el botón correspondiente en la interfaz de usuario no debe estar visible en este estado. No debemos recibir un comando del bus en este estado. Todo esto es cierto siempre que las capas externas y los sistemas desempeñen su función normalmente . Pero en Domain Core no debemos saber sobre la existencia de capas externas y creer en la corrección de su trabajo, debemos proteger a los invariantes del sistema.

Algunas de las comprobaciones se pueden colocar en los Servicios de aplicación en el nivel de validación. Pero esto puede convertirse en programación defensiva , que en casos extremos conduce a lo siguiente:

  1. La encapsulación se debilita, ya que ciertos invariantes deben verificarse en la capa externa.
  2. El conocimiento del área temática "fluye" hacia la capa externa, las verificaciones pueden ser duplicadas por ambas capas.
  3. Validar la ejecución de un comando desde una capa externa puede ser más complejo y menos confiable que verificar que un objeto de dominio no pueda ejecutar un comando en su estado actual.

Además, si colocamos tales comprobaciones en la capa de validación, debemos informar al usuario el motivo del error. Dado que estamos hablando de una operación que no puede realizarse en las condiciones actuales, corremos el riesgo de estar en una de dos situaciones:

  • Le dimos a un usuario común un mensaje que no entendía en absoluto y que iría a soporte de todos modos, al igual que con el mensaje "Se produjo un error inesperado".
  • Le informamos al villano de una manera bastante inteligible por qué no puede realizar la operación que desea realizar y puede buscar otras soluciones.

Pero volvamos al tema principal del artículo. Según todos los indicios, la situación en discusión es excepcional. Nunca debería suceder, pero si sucede, será malo.

Es más lógico en esta situación lanzar una excepción, prometer los detalles necesarios, devolver al usuario un error de la forma general "La operación no es factible", configurar el monitoreo para este tipo de errores y esperar que nunca los veamos.

¿Qué tipo o tipos de excepciones usar en este caso? Lógicamente, este debería ser un tipo de excepción por separado, para que podamos distinguirlo de los demás y para que no sea capturado accidentalmente por el manejo de excepciones desde la capa externa. Tampoco necesitamos una jerarquía o muchas excepciones, la esencia es la misma: ha sucedido algo inaceptable. En nuestros proyectos, creamos un tipo CorruptedInvariantException para esto y lo usamos en situaciones apropiadas.

Casos especiales para aplicaciones web


Una diferencia significativa entre las aplicaciones web de otras (servicios de escritorio, daemons y windows, etc.) es la interacción con el mundo exterior en forma de operaciones a corto plazo (procesamiento de solicitudes HTTP), después de lo cual la aplicación "olvida" inmediatamente lo que sucedió.

Además, después de procesar la solicitud, siempre se genera una respuesta. Si la operación realizada por nuestro código no devuelve datos, la plataforma aún devolverá una respuesta que contiene el código de estado. Si la operación fue abortada por una excepción, la plataforma aún devolverá una respuesta que contiene el código de estado correspondiente.

Para implementar este comportamiento, el procesamiento de solicitudes en plataformas web se crea en forma de canalizaciones. Primero, la solicitud se procesa secuencialmente (solicitud) y luego se prepara la respuesta.

Podemos usar middleware, filtro de acción, controlador http o filtro ISAPI (dependiendo de la plataforma) e integrarnos a esta tubería en cualquier etapa. Y en cualquier etapa del procesamiento de la solicitud, podemos interrumpir el procesamiento y la tubería procederá a formar una respuesta.

Como regla general, ya no implementamos la parte comercial de la aplicación en la arquitectura de canalización, sino que escribimos código que realiza operaciones secuencialmente. Y con este enfoque, es un poco más difícil implementar el escenario cuando interrumpimos la ejecución de la solicitud e inmediatamente procedemos a la formación de la respuesta.

¿Qué tiene que ver todo esto con el manejo de excepciones?

El hecho es que las reglas para trabajar con las excepciones descritas en las partes anteriores del artículo no encajan bien en este escenario.

Las excepciones son malas de usar porque es ir a la semántica.

El uso generalizado de Result lleva al hecho de que lo arrastramos (Result) a través de todas las capas de la aplicación, y al formar la respuesta, necesitamos analizar Result de alguna manera para comprender qué código de estado devolver. También es recomendable generalizar e insertar este código de análisis en Middleware o ActionFilter, que se convierte en una aventura separada. Es decir, el resultado no es mucho mejor que las excepciones.

¿Qué hacer en tal situación?

No construyas un absoluto. Establecemos las reglas para nuestro propio beneficio, y no en detrimento.

Si desea abortar una operación porque su continuación es imposible, entonces lanzar una excepción no tendrá que pasar a semántica. Dirigimos la ejecución a la salida, y no a otro bloque de código comercial.

Si el motivo de la interrupción es importante para determinar el código de estado deseado, se pueden utilizar tipos de excepción personalizados.

Anteriormente, mencionamos dos tipos personalizados que utilizamos: ItemNotFoundException (transformando a 404) y CorruptedInvariant (transformando a 500).

Si verifica los derechos de los usuarios, ya que no se incluyen en el modelo a seguir o en los reclamos, entonces está permitido crear una excepción prohibida personalizada (código de estado 403).

Y finalmente, validación. Todavía no podemos hacer nada hasta que el usuario modifique su solicitud, esta semántica se describe en el código 422 . Entonces interrumpimos la operación y enviamos la solicitud directamente a la salida. Esto también se puede hacer con la excepción. Por ejemplo, la biblioteca FluentValidation ya tiene un tipo de excepción incorporado que le pasa al cliente todos los detalles necesarios para mostrar claramente al usuario lo que está mal con la solicitud.

Eso es todo. ¿Cómo trabajas con excepciones?

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


All Articles