Clean Architecture o Arquitectura Limpia es un término que introdujo Robert C. Martin (Uncle Bob), como un enfoque de diseño de software que se centra principalmente en dos puntos, la separación de responsabilidades y la independencia del dominio.
Para esto distingue dos partes:
La idea es crear sistemas independientes de los detalles, haciendo que el sistema sea fácil de mantener, adaptar y testear.
Independencia de Frameworks: El sistema no debe depender de la existencia de alguna librería de software. Esto permite usar tales frameworks como herramientas, en lugar de tener que encajar el sistema dentro de sus limitaciones.
Testeable: La lógica de negocio puede ser testeada sin la UI, la base de datos, el servidor web, u otros elementos externos.
Independencia de la UI: La UI puede cambiar fácilmente, sin afectar el resto del sistema. Por ejemplo, una web UI podría ser reemplazada por una consola UI, sin cambiar la lógica de negocio.
Independencia de la Base de Datos: La lógica de negocio no está ligada a la base de datos, así que puedes cambiar de Oracle a SQL Server, por ejemplo, sin afectar la lógica de negocio.
Independencia de Cualquier Agente Externo: La lógica de negocio no debe depender de algo externo, como un servicio web o un dispositivo externo.
La arquitectura se representa típicamente en círculos concéntricos, cada uno representando diferentes áreas del software:
Entities (Enterprise Business Rules): Representan los objetos de negocio del sistema. Son las entidades más internas y generalmente contienen las reglas de negocio generales y de alto nivel.
Use Cases (Application Business Rules): Contienen lógica de negocio específica de la aplicación. Orquestan el flujo de datos hacia y desde las entidades, y dirigen esas entidades para usar su lógica de negocio crítica para lograr algo. Pueden interactuar con la capa de adaptador para obtener y almacenar datos.
Interface Adapters (Adaptadores de Interfaz): Esta capa convierte datos en un formato conveniente para los casos de uso y entidades. Esto podría incluir, por ejemplo, convertir datos de la forma que los entrega la base de datos a la forma que los necesitan los casos de uso y entidades.
Frameworks and Drivers (Frameworks y Controladores): Esta es la capa más externa del sistema. Generalmente consiste en cosas que están fuera de control del código, como bases de datos, frameworks web, etc.
Los círculos concéntricos representan diferentes áreas del software, más al interior tiene más alto nivel. Los círculos externos son mecanismos, los internos políticas. La regla que hace que la arquitectura funcione es la regla de la dependencia.
La dependencia del código fluye de afuera hacia adentro. Del exterior al interior. Lo que está adentro no conoce nada de lo que está afuera.
Aquí podemos ver un escenario típico de un sistema Java basado en web que utiliza una base de datos.
El servidor Web toma la entrada del usuario y se lo pasa al Controller (parte superior izquierda).
El Controller empaqueta la información en un objeto plano Java y pasa este objeto a través de la interfaz InputBoundary a UseCaseInteractor.
UseCaseInteractor interpreta esa información y la usa para controlar a las Entities. También usa la interfaz DataAccessInterface para traer la información usada por las Entities desde la base de datos Database.
Al finalizar UseCaseInteractor construye un objeto plano de respuesta OutputData, que se pasa a través de la OutputBoundary al Presenter.
El Presenter vuelve a empaquetar OutputData preparandolo para mostrar sus datos, y carga ViewModel (con Strings formateados para que los vea el usuario).
View solamente acomoda los datos en la vista (un HTML por ejemplo).
Algo a notar son las direcciones de las dependencias, todas apuntan hacia el interior, respetando la regla de la dependencia.
En este caso vamos a usar como modelo un sistema de e-commerce. Las entidades podrían ser productos, pedidos, clientes, etc. Los casos de uso incluirían procesos como agregar un producto al carrito, realizar un pedido, procesar un pago. Los adaptadores de interfaz podrían convertir los datos de un formulario web en formatos adecuados para los casos de uso (lo omitimos por simplicidad). Los frameworks y controladores incluirían la base de datos y el servidor web.
La estructura planteada es la siguiente:
Producto.java
package ecommerce.dominio;
public class Producto {
private String nombre;
private double precio;
public Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
// Getters y setters
}
Pedido.java
package ecommerce.dominio;
public class Pedido {
private Producto producto;
private int cantidad;
public Pedido(Producto producto, int cantidad) {
this.producto = producto;
this.cantidad = cantidad;
}
// Getters y setters
}
AgregarProductoCarritoInteractor.java (Use Case Interactor)
package ecommerce.casosdeuso;
import ecommerce.dominio.Pedido;
import ecommerce.puerto.PedidoOutputBoundary;
import ecommerce.puerto.RepositorioPedidos;
public class AgregarProductoCarritoInteractor {
private RepositorioPedidos repositorio;
private PedidoOutputBoundary presenter;
public AgregarProductoCarritoInteractor(RepositorioPedidos repositorio, PedidoOutputBoundary presenter) {
this.repositorio = repositorio;
this.presenter = presenter;
}
public void agregar(Pedido pedido) {
// Lógica para agregar producto al carrito
repositorio.guardarPedido(pedido);
presenter.presentarConfirmacionPedido(pedido);
}
}
RepositorioPedidos.java (Data Access Interface)
package ecommerce.puerto;
import ecommerce.dominio.Pedido;
public interface RepositorioPedidos {
void guardarPedido(Pedido pedido);
}
PedidoOutputBoundary.java
package ecommerce.puerto;
import ecommerce.dominio.Pedido;
public interface PedidoOutputBoundary {
void presentarConfirmacionPedido(Pedido pedido);
}
PedidoPresenter.java
package ecommerce.presentador;
import ecommerce.dominio.Pedido;
import ecommerce.puerto.PedidoOutputBoundary;
public class PedidoPresenter implements PedidoOutputBoundary {
@Override
public void presentarConfirmacionPedido(Pedido pedido) {
// Simple presentación en consola
System.out.println(“Pedido confirmado: ” + pedido.getProducto().getNombre() + “ x ” + pedido.getCantidad());
}
}
PedidoController.java
package ecommerce.controlador;
import ecommerce.casosdeuso.AgregarProductoCarritoInteractor;
import ecommerce.dominio.Producto;
import ecommerce.dominio.Pedido;
public class PedidoController {
private AgregarProductoCarritoInteractor interactor;
public PedidoController(AgregarProductoCarritoInteractor interactor) {
this.interactor = interactor;
}
public void agregarProducto(String nombre, double precio, int cantidad) {
Producto producto = new Producto(nombre, precio);
Pedido pedido = new Pedido(producto, cantidad);
interactor.agregar(pedido);
}
}
RepositorioPedidosMemoria.java
package ecommerce.adaptadores;
import ecommerce.dominio.Pedido;
import ecommerce.puerto.RepositorioPedidos;
public class RepositorioPedidosMemoria implements RepositorioPedidos {
@Override
public void guardarPedido(Pedido pedido) {
// Guardar el pedido en una estructura de datos en memoria
}
}
Main.java
package ecommerce.main;
import ecommerce.adaptadores.RepositorioPedidosMemoria;
import ecommerce.casosdeuso.AgregarProductoCarritoInteractor;
import ecommerce.controlador.PedidoController;
import ecommerce.presentador.PedidoPresenter;
import ecommerce.puerto.PedidoOutputBoundary;
import ecommerce.puerto.RepositorioPedidos;
public class Main {
public static void main(String[] args) {
RepositorioPedidos repositorio = new RepositorioPedidosMemoria();
PedidoOutputBoundary presenter = new PedidoPresenter();
AgregarProductoCarritoInteractor interactor = new AgregarProductoCarritoInteractor(repositorio, presenter);
PedidoController controller = new PedidoController(interactor);
controller.agregarProducto(“Laptop”, 999.99, 1);
}
}
Esta implementación se ajusta a los principios de la Clean Architecture. El Controller recibe y maneja la entrada del usuario, el Interactor procesa el caso de uso, el Presenter gestiona la presentación de la salida, y los adaptadores manejan la interacción con la base de datos. El Main configura y orquesta el funcionamiento de la aplicación.
La Arquitectura Limpia de Robert Martin da un marco sólido para construir sistemas de software con una alta cohesión y bajo acoplamiento, aplica todas las buenas prácticas (como los principios SOLID), lo que a su vez conduce a aplicaciones más sostenibles, escalables y mantenibles a largo plazo.
Resumiendo la filosofía de la arquitectura limpia: