El problema del acoplamiento arquitectónico
A medida que una aplicación crece, uno de los principales riesgos no es la cantidad de código, sino la cantidad de dependencias que existen entre sus componentes. Cuando la lógica de negocio depende directamente de frameworks, bases de datos, APIs externas o mecanismos concretos de infraestructura, cualquier cambio tecnológico puede propagarse por todo el sistema.
La Arquitectura Limpia (Clean Architecture), propuesta por Robert C. Martin, busca resolver este problema colocando las reglas de negocio en el centro del sistema y aislándolas de los detalles externos.
La idea central puede resumirse en una regla: las dependencias del código deben apuntar hacia las reglas de negocio, nunca desde las reglas de negocio hacia los detalles externos.
Las capas de la Arquitectura Limpia
Clean Architecture organiza el sistema en diferentes niveles de responsabilidad. Existen distintas formas de implementar estas capas, pero conceptualmente podemos dividirlas en cuatro niveles principales.
1. Entidades
Las entidades representan las reglas de negocio más esenciales del sistema. Son los componentes que contienen los conceptos propios del dominio y deben tener el menor número posible de dependencias externas.
Por ejemplo, en un sistema bancario podemos tener una entidad CuentaBancaria:
public class CuentaBancaria {
private final String numero;
private BigDecimal saldo;
public CuentaBancaria(String numero, BigDecimal saldoInicial) {
if (saldoInicial.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("El saldo no puede ser negativo");
}
this.numero = numero;
this.saldo = saldoInicial;
}
public void retirar(BigDecimal cantidad) {
if (cantidad.compareTo(saldo) > 0) {
throw new IllegalStateException("Fondos insuficientes");
}
saldo = saldo.subtract(cantidad);
}
}
La entidad no necesita conocer Spring, Hibernate, HTTP, SQL ni ninguna otra tecnología. La regla de negocio existe independientemente de cómo se almacene o transporte la información.
2. Casos de uso
Los casos de uso contienen las reglas específicas de la aplicación. Su responsabilidad es coordinar las entidades y ejecutar las operaciones que el sistema necesita realizar.
Por ejemplo:
public class RetirarDineroUseCase {
private final CuentaRepository cuentaRepository;
public RetirarDineroUseCase(CuentaRepository cuentaRepository) {
this.cuentaRepository = cuentaRepository;
}
public void ejecutar(String numeroCuenta, BigDecimal cantidad) {
CuentaBancaria cuenta = cuentaRepository.buscarPorNumero(numeroCuenta);
cuenta.retirar(cantidad);
cuentaRepository.guardar(cuenta);
}
}
El caso de uso necesita recuperar y guardar una cuenta, pero no necesita saber si esa operación se realiza mediante MySQL, PostgreSQL, MongoDB o cualquier otro mecanismo.
3. Adaptadores de interfaz
Los adaptadores funcionan como una frontera entre la aplicación y el mundo exterior. Transforman los datos externos para que los casos de uso puedan consumirlos y convierten las respuestas de la aplicación para devolverlas al exterior.
Un controlador HTTP es un ejemplo de adaptador:
@RestController
public class CuentaController {
private final RetirarDineroUseCase retirarDinero;
public CuentaController(RetirarDineroUseCase retirarDinero) {
this.retirarDinero = retirarDinero;
}
@PostMapping("/cuentas/{numero}/retiros")
public void retirar(
@PathVariable String numero,
@RequestBody RetiroRequest request) {
retirarDinero.ejecutar(numero, request.cantidad());
}
}
El controlador conoce HTTP y Spring porque pertenece a una capa externa. El caso de uso, en cambio, no necesita conocer ninguna de esas tecnologías.
4. Infraestructura
La infraestructura contiene los detalles concretos necesarios para ejecutar el sistema: bases de datos, frameworks, servidores, clientes HTTP, sistemas de mensajería, servicios externos y mecanismos de persistencia.
Por ejemplo, podemos implementar el repositorio empleado por el caso de uso:
public interface CuentaRepository {
CuentaBancaria buscarPorNumero(String numero);
void guardar(CuentaBancaria cuenta);
}
Y posteriormente crear una implementación concreta:
public class CuentaRepositoryMySQL implements CuentaRepository {
@Override
public CuentaBancaria buscarPorNumero(String numero) {
// Consulta concreta contra MySQL
return null;
}
@Override
public void guardar(CuentaBancaria cuenta) {
// Persistencia concreta en MySQL
}
}
El caso de uso depende de la abstracción CuentaRepository, mientras que CuentaRepositoryMySQL se encarga de proporcionar la implementación concreta.
La regla de dependencia
La parte más importante de Clean Architecture no consiste en dividir el código en carpetas. La verdadera regla arquitectónica está en la dirección de las dependencias.
Una dependencia debe dirigirse desde los componentes externos hacia los componentes internos:
Infraestructura
|
v
Adaptadores
|
v
Casos de Uso
|
v
Entidades
Esto significa que una entidad no debe depender de un controlador HTTP. Un caso de uso no debe depender directamente de MySQL. Y las reglas de negocio no deben necesitar conocer un framework para funcionar.
Inversión de dependencias
Aquí aparece una relación directa con el principio Dependency Inversion Principle (DIP) de SOLID.
Supongamos que nuestro caso de uso necesita guardar usuarios. En lugar de escribir directamente una dependencia hacia una clase concreta de MySQL:
public class RegistrarUsuarioUseCase {
private final MySQLUsuarioRepository repository;
}
Definimos una abstracción:
public interface UsuarioRepository {
void guardar(Usuario usuario);
}
El caso de uso depende de esa abstracción:
public class RegistrarUsuarioUseCase {
private final UsuarioRepository repository;
public RegistrarUsuarioUseCase(UsuarioRepository repository) {
this.repository = repository;
}
}
La infraestructura puede implementar ese contrato posteriormente:
public class MySQLUsuarioRepository implements UsuarioRepository {
@Override
public void guardar(Usuario usuario) {
// Implementacion concreta
}
}
De esta manera, el núcleo de la aplicación no queda atado a MySQL. La infraestructura se adapta al contrato que necesita el núcleo.
¿Qué sucede si cambiamos la base de datos?
Supongamos que una aplicación trabaja con MySQL y posteriormente necesitamos migrarla a PostgreSQL.
Si los casos de uso dependen directamente de clases relacionadas con MySQL, el cambio puede propagarse por diferentes partes de la aplicación.
Con una abstracción como UsuarioRepository, podemos crear otra implementación:
public class PostgreSQLUsuarioRepository implements UsuarioRepository {
@Override
public void guardar(Usuario usuario) {
// Implementacion concreta para PostgreSQL
}
}
El caso de uso permanece sin modificaciones porque solamente conoce el contrato que necesita.
Los frameworks son detalles
Uno de los conceptos más interesantes de Clean Architecture es considerar los frameworks como detalles externos, no como el centro de la arquitectura.
Spring Boot puede ser excelente para desarrollar una aplicación empresarial, pero las reglas de negocio no deberían existir solamente porque Spring Boot existe.
El mismo principio aplica para Hibernate, Angular, React, Docker, Kubernetes, MySQL, PostgreSQL o cualquier otro componente tecnológico.
Las tecnologías pueden cambiar. El negocio permanece.
Clean Architecture no es una estructura de carpetas
Un error común consiste en pensar que Clean Architecture significa simplemente crear directorios llamados entities, usecases, controllers y repositories.
La estructura de carpetas puede ayudar, pero no garantiza una arquitectura limpia.
Lo verdaderamente importante es que las dependencias respeten las fronteras arquitectónicas. Una clase ubicada dentro de entities que depende directamente de Hibernate sigue estando acoplada a infraestructura aunque su ubicación parezca correcta.
La arquitectura se encuentra en las relaciones entre los componentes, no solamente en sus nombres o directorios.
Beneficios de Clean Architecture
- Independencia de frameworks: Las reglas de negocio pueden mantenerse independientes de frameworks concretos.
- Independencia de la base de datos: La persistencia puede cambiarse mediante nuevas implementaciones de las abstracciones necesarias.
- Facilidad para realizar pruebas: Los casos de uso pueden probarse con implementaciones simuladas de sus dependencias.
- Menor acoplamiento: Los cambios realizados en infraestructura tienen un impacto reducido sobre el núcleo del sistema.
- Mayor mantenibilidad: Las responsabilidades se encuentran separadas mediante fronteras explícitas.
- Mayor flexibilidad tecnológica: Las decisiones sobre frameworks e infraestructura pueden modificarse sin reconstruir las reglas principales del negocio.
El verdadero objetivo
Clean Architecture no pretende eliminar la complejidad de un sistema. Tampoco significa que todas las aplicaciones necesiten decenas de interfaces o cuatro capas claramente separadas.
Su verdadero objetivo es colocar la complejidad en el lugar correcto y proteger las reglas de negocio frente a los detalles que pueden cambiar.
Una base de datos puede sustituirse. Un framework puede quedar obsoleto. Una API externa puede desaparecer. Un mecanismo de comunicación puede cambiar de REST a mensajería.
Las reglas que representan el propósito del sistema deberían tener la menor dependencia posible de esos cambios.
Consideraciones
Clean Architecture no consiste en construir una aplicación con más capas por el simple hecho de tener más capas. Consiste en establecer fronteras claras y controlar rigurosamente la dirección de las dependencias.
Cuando las reglas de negocio se encuentran protegidas de frameworks, bases de datos y servicios externos, el sistema adquiere una propiedad extremadamente valiosa: la capacidad de cambiar sin que cada cambio tecnológico obligue a modificar todo el software.
La arquitectura de un sistema no debería estar definida por la tecnología que usamos para ejecutarlo, sino por las reglas y decisiones que hacen que ese sistema exista.
© Arturo Juárez Monroy