Arquitectura de Software - Arquitectura Limpia

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:

  • el core: lo principal, la parte que da sentido y origen al sistema, la lógica de negocio, los casos de uso.
  • los detalles: todo lo externo, la tecnología, las herramientas (frameworks, UI, base de datos, sistemas externos) permiten que el sistema funcione pero no forman parte del negocio.

La idea es crear sistemas independientes de los detalles, haciendo que el sistema sea fácil de mantener, adaptar y testear.

Principios Fundamentales

  1. 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.

  2. Testeable: La lógica de negocio puede ser testeada sin la UI, la base de datos, el servidor web, u otros elementos externos.

  3. 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.

  4. 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.

  5. Independencia de Cualquier Agente Externo: La lógica de negocio no debe depender de algo externo, como un servicio web o un dispositivo externo.

Estructura de la Clean Architecture

La arquitectura se representa típicamente en círculos concéntricos, cada uno representando diferentes áreas del software:

estructura clean architecture
  1. 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.

  2. 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.

  3. 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.

  4. 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.

La regla de la dependencia

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.

Escenario típico

Aquí podemos ver un escenario típico de un sistema Java basado en web que utiliza una base de datos.

escenario típico clean architecture

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.

Ventajas de la Clean Architecture

  • Flexibilidad y Mantenibilidad: Debido a su separación y desacoplamiento, los componentes del sistema pueden cambiarse o actualizarse con poco impacto en las otras partes del sistema.
  • Facilidad de Pruebas: Las capas internas son independientes de las externas, lo que facilita la realización de pruebas unitarias.
  • Independencia del Framework: Al no depender de bibliotecas pesadas y frameworks, el sistema se vuelve menos propenso a quedar obsoleto rápidamente.

Desventajas de la Clean Architecture

  • Complejidad: Puede ser excesiva para aplicaciones sencillas o pequeñas, aumentando la complejidad innecesariamente.
  • Curva de Aprendizaje: Puede ser difícil de entender y aplicar correctamente, especialmente para equipos no familiarizados con sus principios.
  • Tiempo de Desarrollo: Puede requerir más tiempo para implementar inicialmente debido a la necesidad de crear múltiples capas y abstracciones.

Ejemplo

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:

estructura proyecto arquitectura limpia

Código Java

Entidades (Dominio)

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
    }
              

Casos de Uso (Aplicación)

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);
        }
    }
              

Puertos (Interfaces)

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);
    }
              

Presentadores (Presentación)

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());
        }
    }
              

Controladores (Interfaz de Usuario)

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);
        }
    }
              

Adaptadores (Infraestructura)

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 (Entrada de la Aplicación)

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.

Conclusiones

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:

  • No es tan importante la base de datos que se utilice
  • No son tan importantes las tecnologías Web que se utilicen
  • No son tan importantes los frameworks que se utilicen