Diseño de arquitecturas .NET orientadas a microservicios - Ramón Serrano Valero - E-Book

Diseño de arquitecturas .NET orientadas a microservicios E-Book

Ramón Serrano Valero

0,0

Beschreibung

La mejor arquitectura es aquella que resuelve su problema de software. Ahora, tiene a su disposición el manual con el que aprender a diseñar, de forma sencilla y autónoma, arquitecturas basadas en microservicios. Gracias a este libro, dejará atrás los contratiempos al trabajar con monolitos, como el alto acoplamiento dentro de un mismo sistema o la baja escalabilidad al resolver con la misma arquitectura distintos problemas. Asimismo, aprenderá alternativas que le permitirán aplicar distintas soluciones a multitud de cuestiones y sabrá distinguir cuándo es mejor orientar su arquitectura a microservicios y cuándo mantener su monolito. 1.Conocerá los fundamentos de desarrollo .NET. 2.Construirá cada módulo de que se compone una arquitectura con .NET. 3.Aplicará seguridad a su arquitectura mediante la autenticación y autorización JWT. 4.Sabrá cuándo aplicar una arquitectura monolítica o una orientada a microservicios. 5.Será capaz de modelar arquitecturas limpias cumpliendo la regla de dependencia. 6.Diseñará una arquitectura de microservicios usando distintas tecnologías en cada una. 7.Aprenderá los distintos tipos de comunicación entre microservicios. Además, tras su lectura, conseguirá emplear una arquitectura limpia que le permitirá abstrae del almacén de datos utilizado, separar responsabilidades, crear un código reutilizable y mantenible, dotar a su sistema de un nivel de seguridad basado en token JWT, aprender los patrones más utilizados, así como conocer las bondades del desarrollo .NET, entre muchas otras posibilidades. Es el momento de dar rienda suelta a su creatividad y convertirse en el mejor arquitecto de software. Hágase con el libro, conozca los beneficios que aporta utilizar este tipo de arquitecturas y decida por sí mismo si debe o no aplicarlo. Ramón Serrano Valero: Ingeniero Informático con un máster en Ingeniería de Desarrollo para Dispositivos Móviles y otro en Ciencia de Datos. A lo largo de sus más de 14 años de experiencia en desarrollo .NET, diseñando diferentes arquitecturas software para las distintas necesidades de los clientes y asesorando a otras empresas en la definición de arquitecturas, ha aprendido que no todas las empresas requieren una misma arquitectura y que, en ocasiones, no realizar una sobre ingeniería simplifica la solución, al evitar que crezca su complejidad más de lo necesario.

Sie lesen das E-Book in den Legimi-Apps auf:

Android
iOS
von Legimi
zertifizierten E-Readern

Seitenzahl: 298

Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:

Android
iOS
Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



DISEÑO DE ARQUITECTURAS .NETORIENTADAS A MICROSERVICIOS

Ramón Serrano Valero

DISEÑO DE ARQUITECTURAS .NETORIENTADAS A MICROSERVICIOS

Ramón Serrano Valero

 

 

Diseño de arquitecturas .NET orientadas a microservicios

© 2022 Ramón Serrano Valero

Primera edición, 2022

© 2022 MARCOMBO, S. L.

www.marcombo.com

Diseño de cubierta: ENEDENÚ DISEÑO GRÁFICO

Maquetación: D. Márquez

Corrección: Nuria Barroso

«Cualquier forma de reproducción, distribución, comunicación pública o transformación de esta obra solo puede ser realizada con la autorización de sus titulares, salvo excepción prevista por la ley. Diríjase a CEDRO (Centro Español de Derechos Reprográficos, www.cedro.org) si necesita fotocopiar o escanear algún fragmento de esta obra».

ISBN: 978-84-267-3447-1

Producción del ePub: booqlab

 

 

A la persona más especial de mi vida, mi mujer, Sara de la Vega Ruiz. Gracias a ti saqué las fuerzas para hacer realidad este libro y, aunque para lograrlo sacrifiqué muchos fines de semana pegado a una pantalla, robándote el tiempo, te lo compensaré… Gracias por enseñarme que retándose a uno mismo y superando sus propios límites es como día a día se puede alcanzar una mejor versión de uno mismo. Tú ya eres la mejor persona y versión para mí, no cambies nunca.

Aunque aún no sabes leer, porque cuando escribí este libro aun no estabas entre nosotros, estas en camino… Como todo en esta vida no son problemas informáticos, tu madre y yo, te escucharemos y buscaremos las soluciones junto a ti, nuestra hija Emma, te queremos.

ÍNDICE

Introducción

Motivación

Arquitectura software

Principios solid

S: Principio de responsabilidad única (SRP)

O: Principio abierto/cerrado (OCP)

L: Principio de sustitución de Liskov (LSP)

I: Principio de segregación de interfaces (ISP)

D: Principio inversión de dependencias (DIP)

Beneficios de aplicar SOLID

Proyecto web API base con .NET 5

Puntos de inicio de la aplicación

Ficheros de configuración appsettings

Inyección de dependencias en .NET 5

¿Cómo se definen las dependencias que inyectamos?

Ejemplo inyección de dependencias

Patrón Repository

AutoMapper

LOG

Autenticación JWT (JSON web token)

Autenticación

Autorización de recursos

Acceso anónimo a recursos

Obtención de información del usuario

Autorización basada en roles

Middleware

Obtención valor de un middleware

CQRS:Command query responsibility segregation

MediatR: Patrón MediatR

Docker

Comandos

Versión Docker

Listar imágenes de Docker

Descargar imagen

Crear un contenedor

Listar contenedores

Acceso CLI a contenedor

Contenedor Cassandra

Volúmenes

Eliminar contenedor

Eliminar una imagen

Proyecto de microservicios

Microservicio de autenticación

Contenedor Cassandra

Creación de microservicio de autenticación

Capa dominio - ms.users.domain

Capa infraestructura - ms.users.infrastructure

Capa aplicación – ms.users.application

Capa presentación - ms.users.api

Dockerizar microservicio

Microservicio registro de asistencia de empleados

Capa dominio - ms.employees.domain

Capa infraestructura - ms.employees.infrastructure

Capa aplicación - ms.employees.application

Capa presentación - ms.employees.api

Dockerizar microservicio

Microservicio histórico de asistencia laboral

Contenedor MongoDB

Creación de microservicio histórico de asistencia laboral

Capa dominio - ms.attendances.domain

Capa infraestructura - ms.attendances.infrastructure

Capa aplicación - ms.attendances.application

Capa presentación - ms.attendances.api

Dockerizar microservicio

Comunicación entre los microservicios

Comunicación basada en eventos - RabbitMQ

Comunicación Http

Arquitectura con comunicaciones RabbitMQ y HTTP

Incorporación de RabbitMQ

Productor - Evento empleado creado

Consumidor - Evento empleado creado

Productor - Evento asistencia empleado modificada

Consumidor - Evento asistencia empleado modificada

Comunicación HTTP - Desde microservicio de empleados al de asistencias

Creación de un contenedor RabbitMQ en Docker

Interfaz de RabbitMQ

Probar la comunicación de los microservicios

Reflexiones finales

INTRODUCCIÓN

En este libro conoceremos los fundamentos de desarrollo sobre tecnología .NET, en concreto los concernientes al anteriormente conocido como .NET Core, ahora renombrado por Microsoft como .NET 5 o desde finales de 2021 como .NET 6.

Describiremos los diferentes patrones de diseño que se suelen emplear hoy en día en el desarrollo de arquitecturas limpias, patrón repositorio, CQRS, o MediatR.

Interiorizaremos los principios SOLID, aplicándolos en .NET con casos prácticos.

Aprenderemos a realizar inyección de dependencias y sabremos en qué momento debe ser Singleton, Scoped o Transient, según el tiempo de vida de su instanciación.

Seremos capaces de conocer los pilares sobre los que toda arquitectura debe construirse, sistemas de log, estructura por capas de los proyectos, desacoplamiento, reutilización de código, comunicación entre capas y entre sistemas aislados, etc.

Construiremos sistemas de autenticación y autorización, basados en el token JWT, un estándar muy utilizado hoy en día que aporta mucho a nuestros proyectos.

Utilizaremos distintos sistemas de almacenamiento de datos, tanto en sistemas No SQL como Cassandra o MongoDB o sistemas relacionales como SQL Server.

Conoceremos comandos de Docker, descarga de imágenes, construcción de contenedores, así como la utilidad gráfica de Docker Desktop.

Desplegaremos nuestras aplicaciones en contenedores Docker y los intercomunicaremos entre si mediante Visual Studio y Docker Compose.

Diseño de arquitecturas .NET orientadas a microservicios

Diseñaremos un sistema basado en microservicios, cada uno de ellos aislado y con su propio sistema de almacenamiento, en los que cada uno de ellos utiliza una arquitectura limpia.

Realizaremos distintos tipos de comunicación entre los microservicios desplegados en contenedores, tanto por HTTP utilizando el paquete Refit, como asíncrona por medio de publicación y consumo de eventos en sistemas de cola por medio de RabbitMQ.

Utilizaremos el micro ORM de Dapper como alternativa al ORM de Entity Framework en alguno de los microservicios.

Definiremos nuestros propios middleware como capas intermedias que realizan acciones individuales, antes de llegar incluso a la capa de los controladores de las API, de forma que, si alguna de las capas no le habilita a continuar, la petición no proseguirá.

MOTIVACIÓN

Seguramente en algún momento de nuestra carrera como desarrolladores de software, nos hayamos encontrado con arquitecturas mal diseñadas, mal planteadas desde el inicio.

En ocasiones será a causa de la presión que ejercen otros cargos superiores sobre el equipo de desarrollo, optando por reducir la calidad del producto a costa de reducir tiempo de salida a mercado.

Otras veces será debido a la dejadez del equipo que tengamos a nuestra disposición o la inexperiencia, en la que se reduce la calidad del código que vamos implementando, y es que cuando un equipo no está motivado, no tiene hambre y si no tiene hambre no es posible que la creatividad fluya.

Pues bien, nada más lejos de la realidad, todo empieza por la excelencia de uno mismo, uno no puede exigir calidad a sus compañeros de equipo, cuando él mismo se deja llevar, no le importa tirar líneas de código, crear clases mastodónticas, si total… Nunca se piensa en que el código vaya a mantenerse en el tiempo, ¿no es así? Pensamos que en cuanto a cada línea que escribimos, cuando pasen un par de meses, ya no hablo de años, seguiremos entendiendo el código que redactamos, cuando es poco probable que esto ocurra.

Y es que hay una norma básica que debemos seguir: cada línea que escriba debe aportar valor, ser simple, legible y entendible por cualquier persona que nunca haya trabajado en el mismo código; en definitiva, ha de ser un “código limpio”, término acuñado por el “tío Bob”, que, como veremos más adelante, es la base fundamental de muchos principios que deberemos tener en consideración si queremos convertirnos en artesanos del software.

Si quiere mejorar sus habilidades como desarrollador, precisará una buena aptitud para afrontar retos, para solucionar un problema; seguramente exista más de una solución, y si hay más de una solución, hay más de una persona que se ha podido enfrentar a estos retos y ha propuesto la mejor solución que le funcionó para afrontarlo. Seguramente esta persona primero investigó si ya existía la solución a su problema o alguna que lo solventara en parte, de tal forma que, en caso de existir, seguiría sus pasos… A esto se le conoce como patrones, recursos que, como desarrolladores, debemos conocer, para no reinventar la rueda. Si le sirve una solución y encaja en su problema, seguramente sea la buena, tómela en cuenta.

En ocasiones, sigue una misma nomenclatura con el resto del equipo; acuerde una forma de afrontar cada caso de uso, dedíquele tiempo a simplificar cada clase.

ARQUITECTURA SOFTWARE

Todo empieza cuando nos plantean un problema que debemos resolver mediante software. Empezamos por diseñar y planificar antes de programar nada, cómo debería comportarse nuestro sistema, en base a una serie de requisitos y especificaciones, mediante el uso de patrones de diseño.

Tomando de la mano estas premisas, comienza el proceso de diseño estructural del sistema que queremos implementar, siempre pensando en que, posiblemente, nuestro sistema puede evolucionar el día de mañana y debería ser abierto a extensión y lo más escalable posible.

Una parte importante de la arquitectura es el momento en que definimos en nuestro equipo la forma en que debemos implementar cada módulo, la elección del framework empleado para la persistencia de datos, la comunicación entre capas, la nomenclatura a seguir, el uso unificado de patrones en el sistema, en definitiva, la serie de normas que establecemos y que todo el equipo debe seguir.

De hecho, si cada desarrollador empleara sus propias maneras de definir las clases, de intercomunicar entre capas o utilizase múltiples patrones para resolver cada caso de uso, sería un caos para el core del equipo. Está claro que cada persona tiene su forma de desarrollar, sobre todo porque nunca todos los componentes del equipo tienen la misma experiencia, o han trabajado en equipos distintos y no conocen los mismos patrones de diseño o arquitecturas similares.

La mejor arquitectura que pueda definir es aquella que resuelva su problema; no existe una misma arquitectura que solucione todo, incluso dependiendo del nivel de experiencia del equipo es posible que haya que replantearse la manera de diseñar el software que queremos.

Y es que hacer que nuestra arquitectura sea flexible a cambios y no dependiente entre capas o incluso entre concreciones, puede permitirnos trabajar de manera desacoplada y desarrollarla en un entorno tolerante a modificaciones. No deberíamos depender en nuestra arquitectura de implementaciones concretas, sino de abstracciones, que nos permitan utilizar la inyección de dependencias para servir de la concreción pertinente en el momento que lo requiera. (Más adelante hablaremos de la inyección de dependencias, por ahora quedémonos con el nombre.)

Hay que huir en la medida de lo posible de que como está de moda realizar todo el software orientado a microservicios, siempre, enfocarla en cada aplicación hacia este mismo punto.

Si piensa que su aplicación de 5 años ha quedado obsoleta y es inmantenible, pero desea migrarla a microservicios… Cuidado… Si la aplicación ya es inmantenible en una arquitectura monolítica, es muy probable que también lo sea en microservicios, dado que la orientación a los microservicios implica una complejidad adicional al sistema, sobre todo en cuanto a la intercomunicación entre estos.

Antes de proseguir es importante detenerse momentáneamente en el concepto de “arquitectura monolítica”, una estructura donde lo funcional queda acoplado a un mismo sistema, en que todo el código queda contenido en un mismo repositorio y suele acceder a una única base de datos.

Entre estas arquitecturas, encontramos la arquitectura por capas, arquitecturas limpias, onion o cebolla, hexagonal o incluso la cliente servidor.

De este modo, una misma estructura, al estar resolviendo múltiples problemas, tiene múltiples responsabilidades, que quizá podrían simplificar el sistema dividiendo estas responsabilidades en múltiples monolitos.

Como el equipo de desarrollo suele trabajar sobre el mismo repositorio al mismo tiempo, esto puede implicar que, en caso de resolución de conflictos en el control de código fuente como GIT, invirtamos mucho tiempo diario en resolver conflictos.

Estas arquitecturas surgen para resolver un problema software planteado, pero con el tiempo evolucionan y se van añadiendo nuevos problemas a resolver por la misma estructura, dificultan la escalabilidad del sistema, por lo que, si un día queremos realizar cierta funcionalidad en otro lenguaje porque tiene una serie de librerías que nos facilitan la tarea, nos las veremos para poder incluirlo en nuestro sistema.

Lo bueno de trabajar en un monolito, es que cuando queremos detectar fallos y solventarlos, es mucho más rápido que en otro tipo de arquitecturas, debido a que no hay que lidiar con referencias a otros sistemas, conexiones a otras bases de datos, en definitiva, tenemos menos capas aisladas y, por tanto, la trazabilidad se hace más amigable.

No obstante, si queremos evolucionar nuestro monolito, tendremos que pasar a segmentar por módulos nuestro sistema, como primera opción y, una vez lo tengamos segmentado por módulos, es posible que podamos empezar a pasarlo a una arquitectura de microservicios, en caso de ser necesario.

El problema de un monolito [A0.1], es que, si todas las solicitudes consumen la misma API y fuente de datos, es posible que, si alguno de los servicios expuestos se consume al mismo tiempo con un volumen alto de solicitudes, la base de datos caerá y ello causará que otros servicios que nada tenían que ver con el pico de la carga, se vean afectados y no puedan consultar la base de datos, haciendo que los usuarios vean el rendimiento de la aplicación mermado o incluso caído.

Figura A0.1

Una vez nos veamos en esta tesitura que no prevenimos en tiempo de diseño de la aplicación, es posible que nos planteemos migrar a microservicios; la transición inicial sería pasar el monolito a microlitos.

Un microlito es el estado previo para transformar un monolito a microservicio, donde pueden compartir infraestructura o no. Supongamos que comparten la misma infraestructura, es decir, únicamente hemos separado por módulos el monolito anterior, cada una incluso podría tener su propia API y lenguaje de desarrollo, consiguiendo un módulo para realizar Login y otro módulo para crear cuentas de usuario [A0.2]. Al compartir la misma infraestructura de base de datos, aunque un módulo esté sobrecargado y el otro no, al saturar la base de datos y ser compartida por ambos módulos, seguiríamos teniendo un problema de cuello de botella en la base de datos, que no permitiría retornar respuestas al usuario, al haber saturado la base de datos.

No obstante, hemos ganado en escalabilidad, nuestros servicios están separados y pueden estar implementados en diferentes repositorios y lenguajes o incluso hemos aliviado en cierto modo que nuestros equipos de desarrollo al trabajar en distintos repositorios no tengan que resolver tantos conflictos en GIT.

Figura A0.2

Para mejorar el problema anterior, cada microlito no debería depender de la infraestructura de base de datos. Sin embargo, si un microlito, para dar respuesta a una solicitud, necesitara algún dato conocido del otro microlito, en su base de datos, necesitaría implementar una comunicación entre microlitos de HTTP [A0.3].

De modo que, si el microlito A se comunica con el microlito B solicitando información de la base de datos del microlito B y este está sobrecargado, retornaría un error al microlito A y, por tanto, no podría retornar respuesta al usuario, de modo que seguiría afectándole la latencia y el usuario seguiría notando un bajo rendimiento de la aplicación.

Este tipo de errores de comunicación entre servicios caídos puede resolverse mediante cortocircuitos o circuit breakers, que no es más que un salvavidas, para que, si detecta que un servicio o microlito está caído, no siga enviando comunicaciones a dicho microlito e intente retornar el último valor que tuviera cacheado o un valor por defecto, para que el microlito emisor de la comunicación pueda continuar retornando una respuesta al usuario y no un error.

Al no depender de la infraestructura de la base de datos, ahora hemos ganado en escalabilidad y granularidad.

Figura A0.3

Llegados a este punto, nos queda una alternativa, transformar los microlitos en microservicios, ambos módulos ahora son cada uno un microservicio aislado [A0.4], comunicados por un canal de comunicación o bus de eventos.

El microservicio que quiere comunicarse con el otro publica un evento en el bus, de tal forma que cuando el módulo receptor que se encuentra a la escucha, suscrito a este tipo de eventos, reciba el mensaje, lo procesará y dará una respuesta de forma asíncrona.

Con este tipo de comunicación estaríamos desacoplados y no dependeríamos de una interfaz, sino de un evento de dominio, que el emisor publica en el evento y el receptor subscrito intercepta y lee.

Figura A0.4

Con esto hemos dado un vistazo a los problemas de agrupar todos los problemas o módulos en un mismo monolito y cómo granularizar las responsabilidades, aislando módulos, puede solucionar ciertos problemas que puedan darse en caso de que algún módulo quede sobrecargado, consiguiendo pasar de monolito a microservicio, pero no nos enfoquemos en hacer siempre a partir de ahora todo en microservicios. Los microservicios están ahí para ayudarnos a solventar este tipo de problemas cuando ocurren, pero si la solución no va a tener un volumen alto de peticiones o no va a tener gran escalabilidad, introducir microservicios solo aportará complejidad a la solución.

Volviendo al tema de las arquitecturas, hay una norma básica que debemos cumplir en el diseño de arquitecturas, necesitamos que nuestro software deje de estar acoplado u orientado a un framework y que nuestras capas no dependan ni conozcan el framework utilizado; esto nos permitiría que, si un día cambiamos el framework o simplemente la versión de este, no se vean afectadas cada una de las capas superiores.

Un ejemplo de este uso tan acoplado al framework es la arquitectura por capas [A1.0], donde en el núcleo de la aplicación se encuentra el dominio donde se encapsula además el framework de acceso a datos y tiene una dependencia de la capa de acceso a datos; el resto de las capas conocen las entidades de este framework, el tipo de base de datos empleada y su implementación, de modo que cualquier cambio en el framework afectaría de forma considerable al resto de capas. En definitiva, el núcleo de la aplicación no debería conocer el framework empleado, ya que puede cambiar con el tiempo.

Figura A1.0

Claro está que puede ser resuelta esta dependencia en el modelo por capas tradicional, simplemente haciendo uso de interfaces declaradas en la capa de dominio y del principio SOLID de inversión de control, que veremos más adelante en la sección de principios SOLID.

Para ayudar con la problemática mencionada, Uncle Bob propuso un esquema a seguir para cambiar la dependencia de estas capas; él lo define como arquitecturas limpias o clean architectures.

La denominación de arquitectura limpia tiene como origen un artículo escrito por Robert C. Martin, también conocido como Uncle Bob. Este ingeniero de software americano es autor de otro libro que lleva por título Clean Code y en el que se reflexiona acerca de buenas prácticas y estudio de patrones a la hora de escribir software.

Entendamos por arquitectura limpia [A1.1] aquella que pretende conseguir estructuras modulares bien separadas, de fácil lectura, con código limpio y que puedan realizarse test con facilidad.

Figura A1.1

Si observamos la figura [A1.1], nuestro modelo o lógica de negocio está en el interior, en el corazón del diagrama y por encima de esta capa tenemos la capa de casos de uso, la de lógica de negocio de la aplicación, la de controlador y, finalmente, la capa correspondiente a la vista, base de datos, interfaces externas, en definitiva, librerías que no pertenezcan al sistema y acceso externo.

Además de las diferentes capas, quedémonos con la flecha que vemos atravesar cada capa, se denomina regla de dependencia y nos la propone Uncle Bob. Nos indica que existe un único sentido para atravesarlas, deben apuntar desde el extremo de la figura hacia su núcleo y no a la inversa. Con esta separación, si comparamos dos capas, la capa interior no debe conocer nada de las capas exteriores a ella.

De esta forma conseguimos que nuestra lógica de negocio o casos de uso sean independientes y no empleen ningún elemento de cualquier otra capa externa.

Los diferentes elementos que vemos en la figura [A1.1] serían:

• Entidades: Objetos de negocio de nuestra aplicación, que poseen propiedades y métodos, deben ser reutilizables y con una estructura que varíe escasamente.

• Casos de uso: Son acciones que implementan las reglas de negocio de la aplicación, orquestando el flujo de datos desde y hacia las entidades.

• Adaptadores de interfaz: Transforman los datos desde el formato más conveniente para los casos de uso y entidades, hasta transformarlos en el formato que mejor convenga a “Base de datos” o “Interfaz de usuario”. Los objetos transferidos son objetos DTO (data transfer objects), son simples, sin lógica de negocio, de tal forma que su formato pueda ser transformado por los adaptadores de interfaz al formato que más convenga al resto de capas.

• Frameworks y drivers: Es la capa más externa que debe comunicarse hacia las capas interiores y hacia el exterior del sistema, aquí encontramos el acceso a datos o la publicación de eventos.

Lo que propone es realmente interesante. Se basa en la dirección de la dependencia entre capas, las capas de nivel superior dependen de las capas de nivel inferior, pero no a la inversa. Ello implica un cambio brutal a lo que habíamos aplicado hasta el momento, ahora podíamos cambiar cualquier capa superior, sin que las internas se vieran afectadas por el cambio realizado.

Ahora bien, dentro de las arquitecturas limpias, está la arquitectura hexagonal [A1.2], también conocida como puertos y adaptadores, que ayuda a los desarrolladores a orientar el software hacia el dominio de la aplicación, la organización de código y desacoplamiento entre capas.

Esta arquitectura resuelve el problema de acoplamiento entre proyectos de N-capas y pone en el núcleo del sistema toda la lógica del dominio, definiendo puertos primarios para interacción desde el exterior como son las interfaces de usuario, puertos secundarios de acceso a base de datos o de comunicación con sistemas o servicios externos, haciendo uso de unos adaptadores especializados en implementar dicha funcionalidad, bien sean controladores, acceso a base de datos o envío de emails, etc.

Figura A1.2

En la figura [A1.2] podemos observar cada una de las capas:

•Dominio: Capa más interna del sistema; no debe depender de ninguna otra capa. Podemos encontrar las entidades, interfaces, usadas en inversión de dependencias, que implementa la capa de infraestructura y la lógica de negocio que nosotros mismos consideremos el “core” de nuestra aplicación.

Otro de los recursos a tener en cuenta en esta capa podría ser el de objetos de valor o value objects, clases sencillas, que representan un valor y que pueden contener su propia lógica de negocio, como propia validación. En el momento que hacemos uso de estos recursos, estamos reemplazando, por ejemplo, en lugar de trabajar con un parámetro string como un identificador de usuario, trabajar con un “UserId” que representa internamente un código hash que lleva implícito que no pueda ser nulo, además de otras posibles validaciones como longitud fija del código, etc. Además, nos aporta legibilidad al código, ya que no es lo mismo indicar “string userId” que “UserId userId” o en caso de ser un string que sería algo más genérico, y por tanto podría enviarse cualquier cosa, sin embargo, al ver que es de tipo de dominio personalizado UserId, vemos que dicho string tendrá unas características específicas.

Cabe destacar que podemos definir validaciones a nivel de dominio, como hemos visto con los value objects, sobre tipos de objetos, longitud de atributos, validación de lógica de negocio, etc.

Otro de los elementos que encontraremos en esta capa será el registro de eventos de dominio, a través de los métodos de los modelos de dominio.

En definitiva, los servicios que contienen la implementación de la lógica de negocio definida por nosotros se definirán en esta capa, aunque únicamente debemos definir la lógica en estos servicios si se va a reutilizar por otros servicios de aplicación o casos de uso.

•Aplicación: Capa que depende de la capa de dominio de la aplicación, y por tanto puede hacer uso de las entidades definidas en la capa de dominio, así como los contratos o interfaces definidas en dicha capa. Aquí se originan todas las entradas a nuestro sistema; podemos encontrar, además, los casos de uso, es decir, acciones que nuestro sistema realiza; el conjunto de estas acciones compone el funcional de la aplicación.

Incluimos en esta capa los servicios de aplicación, responsables de invocar otros servicios de la capa de dominio, reutilizables por la capa de aplicación, además de gestionar las transacciones o publicar eventos de dominio.

Debemos, además, considerar que un servicio de aplicación deberá hacer uso de las interfaces definidas en la capa de dominio, para poder invocar a la implementación de repositorios que se encuentran en la capa de infraestructura, haciendo uso de la inversión de control (principio SOLID).

•Infraestructura: Esta capa depende de la capa de dominio, es aquí donde incluiremos todo aquello que ejecute cambios hacia el exterior del sistema. Como, por ejemplo, la implementación de acceso a datos, el envío de eventos hacia otros sistemas, o el uso de paquetes externos a nuestro sistema que implementamos fuera del dominio de la aplicación; de esta forma, no intoxicaremos nuestro dominio y nos desacoplamos de esta capa.

Por este motivo, aquí encontraremos la implementación de los repositorios, que serán accesibles por otras capas, haciendo uso de interfaces definidas en la capa de dominio, para que, en caso de que un día modifiquemos dicha implementación, el resto de las capas no se vean afectadas por dicho cambio, como versiones de bases de datos o incluso cambio de persistencia de base de datos.

Observemos, pues, que la regla de la dependencia sigue activa al tratar como arquitectura limpia; la dependencia sería desde la capa más externa, infraestructura, pasando por la de aplicación y acabando en el dominio del sistema. O, lo que es lo mismo, la regla de fuera hacia dentro.

Esto implica que no deberíamos encontrar en la capa dominio ninguna referencia que no sea a sí misma, ni la capa de aplicación debería tener referencias distintas de la capa de dominio.

Y ¿de qué forma el dominio o lógica de negocio puede conocer la capa externa de infraestructura? Realmente no la conoce; simplemente, al tener definida la interfaz que implementa la infraestructura, la capa de aplicación o las reglas de negocio del dominio pueden llegar a invocar a los repositorios de acceso a datos y, por tanto, ejecutar su implementación. Para ello hace uso de la inyección de dependencias, para resolver la interfaz por una implementación concreta, y de la inversión de dependencias de los principios SOLID.

De hecho, si no definiéramos esta interfaz en el dominio, cada capa debería invocar a la concreción, creando dependencias entre capas, y, por tanto, no solo rompería la regla de dependencia, sino que cualquier cambio en la implementación afectaría al resto de capas.

Como hemos visto, la arquitectura hexagonal es solo un ejemplo de arquitectura limpia, pero podríamos introducir más o menos capas, siempre que cumplamos la regla de la dependencia y que las capas internas no conozcan las capas externas y siempre que el dominio lo respetemos, podremos variar este modelo.

Hemos hablado de algunas de las distintas arquitecturas más empleadas hasta el momento, sin mencionar la que está de actualidad, es decir, arquitecturas basadas en microservicios; aunque parezca algo desconocido, en realidad es un conjunto de monolitos que han surgido para dar solución a los megamonolitos que cuando crecen se hacen inmantenibles y aglutinan en un mismo punto diferentes responsabilidades.

Para dar solución a este problema, surge este tipo de arquitecturas, que nos permiten crear monolitos muy pequeños, que acceden a su propia parcela de base de datos, incluso tienen acotada la responsabilidad y realizan una o escasas funcionalidades con la tecnología y lenguaje que mejor se adapte a resolver el miniproblema.

Hablar de microservicios puede parecer la solución a todos nuestros problemas, pero nada más lejos de la realidad, cuando tratemos con microservicios, debemos saber que para una aplicación básica sin apenas escalabilidad y sin múltiples personas trabajando en el mismo software, quizá no sea la mejor solución.

Podemos pensar que los monolitos han quedado obsoletos, creemos que nuestro monolito funciona de manera inadecuada y pensamos que migrarlo a microservicios resolverá nuestros problemas. La realidad es que no, los microservicios mejoran, ayudan, pero siempre en un contexto que tenga sentido para aplicar esta arquitectura.

Es probable que, si en monolito nuestra solución funcionaba mal, seguramente, en microservicio, nuestra aplicación seguirá funcionando mal. Esto se debe a que introducir microservicios introduce mayor complejidad al sistema del que ya había actualmente, ahora habrá módulos trabajando de forma aislada y la comunicación entre microservicios será un reto para resolver.

No obstante, será una de las arquitecturas que tratemos en este libro, dado que está bien conocerla para saber cuándo debemos utilizarla.

Para finalizar, cabe destacar que realizar un buen diseño, nos permitirá añadir nuevas funcionalidades al sistema, sin que crezca la complejidad de este. No sobrediseñemos añadiendo complejidad al sistema, si la esencia del problema puede ser resuelta por la arquitectura base.

Un error común de introducir complejidad que no necesita nuestro sistema es cuando implementamos un borrador de una funcionalidad y conseguimos que funcione, es probable cometer el error de dejarlo como código bueno, aunque no cumpla las normas establecidas en el sistema, es decir, añadimos complejidad innecesaria, cuando lo que deberíamos hacer es refactorizar y transformar dicho código a la arquitectura, sin introducir elementos que no sean indispensables.

Y como no podía ser de otra forma, toda arquitectura, cuando actuemos como arquitectos de software, debería basarse en una serie de principios, estos son los principios SOLID, que veremos a continuación.

PRINCIPIOS SOLID

Hemos ido introduciendo el concepto sin haber entrado en detalle, pero estos principios son bien conocidos y existe mucha literatura al respecto, que debemos leer. Los principios SOLID fueron una de las aportaciones del ingeniero de software Robert Cecil Martin, también conocido como “Uncle Bob” a la comunidad de artesanos del software, una serie de principios de diseño orientado a objetos, que todo desarrollador debería conocer y aplicar.

Veamos una serie de ejemplos de implementación en .NET Core 5 y lenguaje C#, para llegar a entender mejor como aplicar estos principios en el mundo real.

• SRP: Principio de responsabilidad única

• OCP: Principio abierto/cerrado

• LSP: Principio de sustitución Liskov

• ISP: Principio de segregación de interfaz

• DIP: Inversión de Dependencias

Procede de uno de los principios de diseño orientado a objetos, basado en el desacoplamiento. En él se establece que módulos de nivel superior no deberían depender de otros de nivel inferior, sino que ambos deberían depender de abstracciones, es lo que se conoce como bajo acoplamiento, cuanto menor, mejor.

Dicho de otro modo, las clases deberían desentenderse de la implementación concreta de una clase. ¿Cómo? Sirviéndose de abstracciones o interfaces haciendo uso de la inyección de dependencias.

Esta definición en la teoría queda genial; sin embargo, hasta que no veamos un ejemplo práctico, costará comprenderlo.

S: Principio de responsabilidad única (SRP)

Es el primer principio de SOLID, en el que se defiende que una clase únicamente debe representar una única responsabilidad; para Robert C. Martin: “Una clase únicamente debe tener una razón para cambiar”.

Es decir, no deberíamos tener una clase que realiza más de una acción, ¿cómo podemos lograr no infringir este principio?

• Cuando encontremos más de un método público en la misma clase, es muy probable que dicha clase tenga más de una responsabilidad.

• Cuando existan varios atributos de una clase en el que unos pocos siempre sean utilizados por una clase y el resto por otra distinta, esto implica que dicha clase podría haberse dividido en otras dos, separando responsabilidades.

• Dificultad de testing sobre el método de una clase, puede causar que el método deba segmentarse.

Una vez consigamos separar por responsabilidades, nuestro código incrementará la reutilización, pudiendo invocar desde múltiples servicios estas funcionalidades.

Al tener clases más pequeñas, que únicamente realizan una responsabilidad, conseguimos reducir la duplicidad del código y ganamos en legibilidad, ¿no es genial?

Veamos un ejemplo donde se infringe este principio [S5.3]; Tenemos una clase llamada UserRepositorySrpViolation, que podemos observar en la línea 5 del código. Esta clase posee dos métodos públicos, uno para registrar el usuario y otro método para enviar un email de bienvenida al usuario que se acaba de registrar.

De modo que, tras registrar el usuario de manera satisfactoria en la base de datos, automáticamente se envía un email.

Figura S5.3

Figura S5.3.a

Al disponer de dos métodos públicos en la misma clase, tenemos dos puntos de entrada a la clase y por tanto dos posibles responsabilidades, ello conduce al incumplimiento del principio [S5.3.a]. Pero…entonces… ¿existe alguna manera de evitar esto?

La respuesta, como no podía ser de otra manera, es sí. Simplemente intentemos separar [S5.4] de la clase UserRepository el método que no pertenece al propio dominio de usuario, es decir, el envío de email SendWelcomeMessageToMail.

Figura S5.4

Para ello creamos una nueva clase notificadora de emails EmailSenderWelcomeMessage, que seguirá teniendo el mismo método antiguo de envío de emails.

Posteriormente, en la clase UserRepository inyectamos la clase notificadora de emails que acabamos de crear a través de su constructor; de esta forma, podremos invocar el método de la instancia _emailSenderWelcomeMessage, por lo que seguiremos enviando el mismo email tras el registro de usuario.

Figura S5.4.a

Con esto hemos conseguido que UserRepository [S5.4.a] tenga una cohesión más alta y con una única responsabilidad. Por lo que ganamos en desacoplamiento.

¿Y cuál es la diferencia? La diferencia principal es que ahora ambas clases tienen una única responsabilidad, la clase repositorio de usuario únicamente posee un método de entrada y la clase envío de email también, con lo cual ganamos en legibilidad, testeo y reutilización, es decir, ambas clases tienen que hacer lo que han de hacer y nada más.

Pero podemos ir un paso más allá y ganar en modularidad, la posibilidad de que varias clases puedan implementar el mismo método que defina su interfaz [S5.5].

Figura S5.5

Para ello, creamos una interfaz ISender, que implemente un método SendTo, así como dos clases SendWelcomeMessageByPushNotification, que envía una notificación push al móvil y SendWelcomeMessageByEmail, que envía un email al usuario registrado.

Ambas clases implementan la interfaz ISender, lo cual nos permite ganar en modularidad y que podamos crear tantos tipos de mensajería como queramos, ya que cada clase implementará su forma de comunicación.

Ahora ya únicamente en la clase UserRepositoryModular recibiremos por parámetro en el constructor una clase que implemente dicha interfaz, con lo cual, si le proporcionásemos la clase de envío de emails, la línea 17 de UserRepositoryModular, implementaría el envío definido en la clase SendWelcomeMessageByEmail; no obstante, si por el contrario, se inyectase la clase de notificaciones push, la línea 17, implementaría el envío push mediante el método SendTo. ¿Increíble las ventajas que puede traernos la modularidad?

O: Principio abierto/cerrado (OCP)

Este principio establece que nuestras clases deben ser abiertas para extensión, pero cerradas para modificación.

Uno de los principales motivos es no “romper” aquello que ya funcionaba, por el simple hecho de haber introducido una nueva funcionalidad o simplemente cambio sobre la clase en cuestión.

No cumplir este principio puede hacer que nuestro equipo de desarrollo entre en bucle de regresión tratando de probar casos de uso que con anterioridad al cambio las pruebas pasaban y posteriormente debemos volver a probarlas.

Para no infringir este principio, la clave es evitar la dependencia de concreciones, es decir, clases concretas, abstraernos de su implementación. No modificar una clase ya implementada, se debe hacer extensible mediante la herencia, empleando para ello abstracciones como clases abstractas o interfaces.

En otras palabras, no dependa en la implementación del método de una clase, de un tipo en concreto, dado que, si un día cambia su tipo o simplemente incorporamos uno nuevo, nos veríamos obligados a introducir múltiples condicionales “if else” o un “switch case” y, por tanto, estaríamos modificando la implementación del método incumpliendo el principio.

Si conseguimos desarrollar desacoplando los métodos de la clase, de una implementación concreta, podremos incorporar nuevos casos de uso al extender su funcionalidad.

Veamos con un ejemplo cómo se incumple este principio [S5.6]; disponemos de una tienda virtual, representada por la clase VirtualShop,