Separación de lógica de negocio mediante Casos de Uso

  • Distinción fundamental entre la lógica de negocio pura, la de aplicación y los detalles de implementación de infraestructura.
  • Implementación de Casos de Uso como interactores independientes para garantizar la responsabilidad única y la testabilidad.
  • Uso de interfaces genéricas y despachadores para optimizar la inyección de dependencias y evitar controladores saturados.

Separación de lógica de negocio mediante Casos de Uso

A menudo, cuando nos metemos en el mundo del desarrollo, nos topamos con que el término «lógica de negocio» se lanza a la ligera en cualquier reunión, usándose para describir desde un simple if en el frontend hasta la gestión compleja de una base de datos. Esta ambigüedad no es moco de pavo, ya que puede llevar a crear sistemas donde todo está mezclado, resultando en ese código espagueti que nos quita el sueño cuando toca hacer una actualización.

Para evitar estos dolores de cabeza, es fundamental entender que no todo el código que toma decisiones es igual. Al separar las responsabilidades mediante patrones como los Casos de Uso o la Clean Architecture, logramos que el corazón de la aplicación sea robusto y no dependa de si usamos SQL Server, MongoDB o si la interfaz es una web o una app móvil. Vamos a desgranar cómo montar este sistema para que tu software sea realmente escalable.

Desmontando el concepto de Lógica

Para empezar a limpiar el código, hay que diferenciar los tipos de decisiones que toma la máquina. La lógica de negocio es aquella que representa la realidad del mundo real; son las reglas que un experto del sector te daría sin saber nada de programación. Por ejemplo, que un cliente con deudas superiores a mil dólares no pueda pedir más crédito es una regla de negocio pura, independientemente de que se guarde en un JSON o en una tabla de Oracle.

Por otro lado, tenemos la lógica de aplicación, que es más bien la orquestadora. Su función es preparar el escenario: buscar el dato en la base de datos, llamar a la regla de negocio y luego guardar el resultado. Es lo que algunos llaman el «método bocadillo», donde tenemos una capa impura de entrada, el núcleo puro de negocio en el medio y otra capa impura de salida.

Finalmente, están los detalles de implementación. Aquí entra la lógica de presentación (como validar que un email tenga el formato correcto o decidir si un botón se muestra o no) y la de infraestructura (la comunicación técnica con APIs externas o el sistema de archivos). Estos elementos son detalles secundarios que deberían poder cambiarse sin tocar ni una sola línea del núcleo del sistema.

El problema de las interfaces monstruo

En muchos proyectos es habitual crear una interfaz de servicio gigante, como una IClientService, que contiene absolutamente todas las acciones posibles de un cliente. Al principio parece una buena idea porque seguimos los principios SOLID, pero con el tiempo esa clase se convierte en un monstruo de miles de líneas que es un auténtico calvario de leer y mantener.

La solución pasa por ganar mayor granularidad. En lugar de una interfaz masiva, podemos fragmentar cada acción en una clase independiente. Aquí es donde entran los Casos de Uso: cada clase representa una acción única y específica que el usuario puede realizar, asegurando que cada componente tenga una sola responsabilidad.

Implementando Casos de Uso de forma eficiente

Para evitar depender de clases concretas y mantener la inversión de control, lo ideal es definir una interfaz genérica, por ejemplo IUseCase<TRequest, TResponse>. De este modo, estandarizamos la forma en que cada caso de uso recibe datos y devuelve una respuesta, eliminando la necesidad de crear decenas de interfaces individuales para cada acción del sistema.

Para que esto sea aún más limpio, podemos introducir un despachador de casos de uso. Esta capa extra se encarga de resolver internamente qué caso de uso debe ejecutarse basándose en la petición, evitando que el controlador se llene de inyecciones de dependencias infinitas y dejando el código del controlador mucho más despejado.

Ventajas de este enfoque arquitectónico

  • Testabilidad superior: Al estar la lógica aislada de la base de datos y la UI, puedes escribir tests unitarios que se ejecuten en milisegundos sin necesidad de levantar un servidor.
  • Independencia tecnológica: Puedes migrar tu base de datos o cambiar el framework de frontend sin que la lógica fundamental del negocio se vea afectada.
  • Código autodocumentado: Basta con mirar la carpeta de Casos de Uso para saber exactamente qué hace la aplicación sin tener que leer el código interno.
  • Reutilización real: Un mismo caso de uso puede ser disparado por una API REST, una tarea programada o una consola de comandos.

Buenas prácticas para un diseño robusto

Para que este sistema no se degrade, es vital que los Casos de Uso no dependan unos de otros evitando así las dependencias circulares. También es recomendable que devuelvan modelos de dominio y no entidades directas de la base de datos, manteniendo así la estanqueidad de las capas.

En cuanto al manejo de errores, lo más elegante es utilizar objetos de resultado (Result Pattern) en lugar de lanzar excepciones para controlar el flujo del negocio. Esto hace que el camino del código sea más predecible y evita que la aplicación se rompa por errores que eran estrictamente previstos por la lógica.

La adopción de una estructura basada en interactores y la correcta segregación entre el núcleo del sistema y los detalles técnicos permiten que el software sea flexible ante los cambios del mercado. Al priorizar la responsabilidad única y el desacoplamiento, transformamos aplicaciones rígidas en sistemas ágiles que facilitan el trabajo en equipo y aseguran una calidad de código sostenible a largo plazo.


Add as preferred source