====== 01 - Arquitecturas de software ====== Podemos clasificar una aplicación atendiendo a distintos criterios. Estas decisiones se combinan: por ejemplo, una aplicación puede desplegarse como monolito modular y organizarse internamente por capas con inversión de dependencias. ===== 1. Principios de arquitectura limpia ===== Las arquitecturas limpias protegen las reglas de negocio frente a los detalles técnicos y externos. * **Independencia de frameworks:** el framework es una herramienta, no el centro de la aplicación. * **Testeable:** las reglas de negocio pueden probarse sin levantar la interfaz, la base de datos ni el framework. * **Independencia de la interfaz de usuario:** la misma lógica puede exponerse mediante interfaces distintas. * **Independencia de la base de datos:** se puede sustituir la tecnología de persistencia sin reescribir las reglas de negocio. * **Independencia de agentes externos:** las reglas de negocio no dependen de servicios externos concretos. * **Regla de dependencia:** las dependencias del código apuntan hacia las políticas y reglas de negocio, no hacia los detalles. * **Inversión de dependencias (DIP):** las abstracciones que necesita el núcleo pertenecen al núcleo; los detalles externos las implementan. @startuml allowmixing package "controller" #A5D6A7 { class Controller } package "domain" #EF5350 { class UseCase class Model interface RepositoryPort } package "persistence" #90CAF9 { class RepositoryAdapter } database "Base de datos" as db Controller --> UseCase : invoca UseCase --> Model : aplica reglas UseCase --> RepositoryPort : usa abstraccion RepositoryAdapter ..|> RepositoryPort : implementa RepositoryAdapter --> db : accede @enduml **Arquitectura limpia** es un concepto general: busca proteger las reglas de negocio y dirigir las dependencias hacia el núcleo. La arquitectura hexagonal, la arquitectura de cebolla y la arquitectura por capas con inversión de dependencias (DIP) son distintos enfoques de arquitectura limpia. ===== 2. Organización interna ===== ==== 2.1 Arquitectura por capas ==== Organiza el código según responsabilidades. En aplicaciones web es habitual distinguir estas capas: ^ Capa ^ Responsabilidad ^ | ''controller'' | Recibir solicitudes, coordinar la entrada y producir respuestas | | ''domain'' | Contener servicios, casos de uso y reglas del negocio | | ''persistence'' | Implementar el acceso a datos y comunicarse con la base de datos | En una arquitectura por capas tradicional, ''controller'' llama a ''domain'' y este utiliza ''persistence''. Si el dominio depende directamente de clases concretas de persistencia, queda acoplado a la tecnología. Cuando ''domain'' define las interfaces que necesita y ''persistence'' las implementa, se invierte la dependencia respecto a la persistencia y se aplican principios de arquitectura limpia. Las capas son una división lógica, no una estructura rígida. Se pueden añadir capas o separar componentes según las necesidades; por ejemplo, los casos de uso pueden estar separados de los servicios de dominio. @startuml allowmixing package "controller" #A5D6A7 { class Controller } package "domain" #EF5350 { class Service class Model interface RepositoryPort } package "persistence" #90CAF9 { class RepositoryAdapter } database "Base de datos" as db Controller --> Service Service --> Model Service --> RepositoryPort : usa RepositoryAdapter ..|> RepositoryPort : implementa RepositoryAdapter --> db @enduml ==== 2.2 Arquitectura hexagonal (Ports and Adapters) ==== Separa el núcleo de la aplicación de los sistemas externos mediante puertos y adaptadores. * **Puerto:** abstracción que describe cómo se usa el núcleo o qué necesita del exterior. Por ejemplo, una interfaz de repositorio. * **Adaptador:** conecta un puerto con una tecnología concreta. Por ejemplo, un ''controller'' REST o una implementación JPA. * **Dirección de dependencia:** el adaptador implementa o invoca el puerto; el dominio no conoce el adaptador. @startuml allowmixing package "Adaptadores de entrada" #A5D6A7 { class Controller } package "domain" #EF5350 { interface UseCase class Model interface RepositoryPort interface NotificationPort } package "persistence" #90CAF9 { class RepositoryAdapter } package "Adaptador de servicio externo" #90CAF9 { class EmailAdapter } database "Base de datos" as db cloud "Servicio de correo" as mail Controller --> UseCase : invoca UseCase --> Model : aplica reglas UseCase --> RepositoryPort : usa puerto RepositoryAdapter ..|> RepositoryPort : implementa RepositoryAdapter --> db UseCase --> NotificationPort : usa puerto EmailAdapter ..|> NotificationPort : implementa EmailAdapter --> mail @enduml ==== 2.3 Comparativa de enfoques internos ==== ^ Enfoque ^ Idea principal ^ Observación ^ | Capas tradicional | Agrupar por responsabilidad y delegar entre capas | Sencillo; vigilar el acoplamiento hacia infraestructura | | Hexagonal | Aislar el núcleo mediante puertos y adaptadores | Facilita sustituir tecnologías y probar el núcleo | | Cebolla | Organizar el sistema en anillos alrededor del dominio | Las dependencias apuntan hacia el centro | ===== 3. Arquitectura de despliegue ===== Describe cómo se empaqueta, ejecuta y despliega la aplicación. Es un criterio distinto de la organización interna. ==== 3.1 Monolito, monolito modular y microservicios ==== ^ Estilo ^ Despliegue ^ Límites y datos ^ Ventajas y costes ^ | Monolito | Una aplicación desplegable | Los componentes suelen compartir proceso y almacenamiento | Operación sencilla; puede crecer con alto acoplamiento | | Monolito modular | Una aplicación desplegable | Módulos con responsabilidades, datos encapsulados y APIs internas explícitas | Mantiene simplicidad operativa y mejora la modularidad | | Microservicios | Varios servicios desplegables | Cada servicio controla sus datos; se comunican por red | Despliegue y escalado independientes; más complejidad operativa | * **Monolito** no significa necesariamente código desordenado: puede tener módulos bien diseñados. Es una opción adecuada para aplicaciones pequeñas y poco complejas, como un CRUD sencillo. * **Monolito modular:** los módulos colaboran dentro de un mismo proceso mediante APIs explícitas. Las fronteras limitan dependencias y acceso a datos internos. * **Microservicio:** servicio autónomo que se despliega por separado y es responsable de sus datos. No basta con separar una aplicación en varios proyectos. No hay que confundir **cómo se organiza el código** (por capas, hexagonal, cebolla) con **cómo se despliega** (monolito, monolito modular, microservicios). Un monolito puede aplicar principios de arquitectura limpia. @startuml allowmixing node "Monolito: un despliegue" { component "Aplicacion\n(modulos juntos)" as monolith database "Base de datos" as monodb monolith --> monodb } node "Monolito modular: un despliegue" { package "Aplicacion" { component "Modulo A\n(API publica)" as moduleA component "Modulo B\n(API publica)" as moduleB } database "Base de datos compartida\n(datos propiedad de cada modulo)" as modulardb moduleA --> modulardb : solo sus datos moduleB --> modulardb : solo sus datos moduleB --> moduleA : API interna } node "Microservicios: varios despliegues" { component "Servicio A" as serviceA component "Servicio B" as serviceB component "Servicio C" as serviceC database "BD A" as dbA database "BD B" as dbB database "BD C" as dbC serviceA --> dbA serviceB --> dbB serviceC --> dbC serviceC --> serviceB : HTTP / mensaje } @enduml Microservicios no son simplemente módulos separados en carpetas o proyectos: cada servicio debe poder desplegarse y evolucionar de forma autónoma, y la comunicación pasa por la red. ==== 3.2 Tipos de comunicación ==== La comunicación entre componentes puede ser síncrona o asíncrona. La forma concreta depende de si los componentes están dentro del mismo proceso o son servicios distribuidos. ^ Tipo ^ Funcionamiento ^ Características ^ | Síncrona | El emisor hace una llamada y espera una respuesta | Directa; el emisor depende de la disponibilidad y el tiempo de respuesta del receptor | | Asíncrona | El emisor publica un evento o mensaje y no espera a que se complete su procesamiento | Desacopla temporalmente; puede haber retrasos, reintentos y entregas duplicadas | === Comunicación entre módulos === * Los módulos de una misma aplicación pueden comunicarse de forma **síncrona**, mediante llamadas directas a las APIs públicas de otros módulos. * También pueden comunicarse de forma **asíncrona**, publicando eventos que otros módulos consumen. * Los eventos internos no implican comunicación de red ni crean por sí solos aislamiento o una transacción independiente. === Comunicación entre microservicios === * Los microservicios se comunican a través de la **red**. * La comunicación **síncrona** suele realizarse mediante peticiones HTTP y respuestas. * La comunicación **asíncrona** se realiza publicando mensajes o eventos en un intermediario (broker), como RabbitMQ o Kafka; los consumidores los procesan posteriormente. * En cualquiera de los estilos distribuidos, hay que contemplar fallos de red. Con mensajería, los consumidores deben ser idempotentes para tolerar entregas duplicadas. @startuml actor Cliente participant "Servicio A" as ServiceA participant "Servicio B" as ServiceB queue "Broker de mensajes" as Broker participant "Consumidor" as Consumer group Comunicacion sincrona entre modulos (mismo proceso) ServiceA -> ServiceB : API publica (llamada directa) ServiceB --> ServiceA : resultado end group Comunicacion sincrona entre microservicios (HTTP) Cliente -> ServiceA : solicitud ServiceA -> ServiceB : petición HTTP ServiceB --> ServiceA : respuesta ServiceA --> Cliente : resultado end group Comunicacion asincrona entre componentes (eventos) ServiceA -> Broker : publicar evento Broker -> Consumer : entregar evento Consumer --> Broker : confirmar procesamiento end @enduml La mensajería desacopla al emisor del momento en que se procesa el evento, pero no garantiza que cada mensaje se procese exactamente una vez. Diseña consumidores idempotentes. Durante el curso estudiaremos las capas y sus componentes en temas separados. Como referencia, seguiremos una arquitectura por capas desplegada como monolito modular.