Ir al contenido principal

DDD, Hexagonal, Onion, Clean, CQRS,… Cómo juntar todo

Este artículo fue originalmente escrito por Herberto Graca en su blog personal, disponible en https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/. Tanto el contenido del texto como las imágenes son de su autoría.

Prefacio del autor

Esta publicación es parte de Las Crónicas de la Arquitectura de Software, una serie de publicaciones sobre la arquitectura de software. En ellos, escribo sobre lo que he aprendido sobre la arquitectura de software, cómo lo pienso y cómo utilizo ese conocimiento. El contenido de esta publicación podría tener más sentido si lee las publicaciones anteriores de esta serie.

Después de graduarme de la Universidad, seguí una carrera como profesor de secundaria hasta que hace unos años decidí dejarla y convertirme en desarrollador de software a tiempo completo.

A partir de entonces, siempre he sentido que necesito recuperar el tiempo “perdido” y aprender lo más posible, lo más rápido posible. Así que me he vuelto un poco adicto a la experimentación, la lectura y la escritura, con un enfoque especial en el diseño y la arquitectura de software. Por eso escribo estas publicaciones, para ayudarme a aprender.

En mis últimas publicaciones, he estado escribiendo sobre muchos de los conceptos y principios que he aprendido y un poco sobre cómo razono sobre ellos. Pero veo estos como piezas de un gran rompecabezas.

Introducción

La publicación de hoy trata sobre cómo encajo todas estas piezas y, como parece que debería darle un nombre, lo llamo Arquitectura explícita. Además, todos estos conceptos han "superado sus pruebas de batalla" y se utilizan en el código de producción en plataformas muy exigentes. Una es una plataforma SaaS e-com con miles de tiendas web en todo el mundo, otra es un mercado, en vivo en 2 países con un bus de mensajes que maneja más de 20 millones de mensajes por mes.

  • Bloques fundamentales del sistema
  • Herramientas
  • Conexión de las herramientas y los mecanismos de entrega al núcleo de la aplicación
    • Puertos
    • Adaptadores primarios o impulsores
    • Adaptadores secundarios o controlados
    • Inversión de control
  • Organización del núcleo de la aplicación
    • Capa de aplicación
    • Capa de dominio
      • Servicios de dominio
      • Modelo de dominio
  • Componentes
    • Desacoplamiento de componentes
      • Activando la lógica en otros componentes
      • Obtener datos de otros componentes
        • Almacenamiento de datos compartido entre componentes
        • Almacenamiento de datos segregado por componente
  • Flujo de control

 Bloques fundamentales del sistema

Empiezo recordando las arquitecturas EBI y Puertos y Adaptadores. Ambos hacen una separación explícita de qué código es interno a la aplicación, qué es externo y qué se usa para conectar código interno y externo.

Además, la arquitectura de Puertos y Adaptadores identifica explícitamente tres bloques fundamentales de código en un sistema:

  • Qué hace posible ejecutar una interfaz de usuario, sea cual sea el tipo de interfaz de usuario que sea;
  • La lógica empresarial del sistema, o núcleo de la aplicación, que utiliza la interfaz de usuario para hacer que las cosas sucedan;
  • Código de infraestructura, que conecta el núcleo de nuestra aplicación con herramientas como una base de datos, un motor de búsqueda o API de terceros.

El núcleo de la aplicación es lo que realmente debería interesarnos. Es el código que permite que nuestro código haga lo que se supone que debe hacer, ES nuestra aplicación. Puede usar varias interfaces de usuario (aplicación web progresiva, móvil, CLI, API, ...) pero el código que realmente hace el trabajo es el mismo y está ubicado en el núcleo de la aplicación, no debería importar realmente qué interfaz de usuario lo activa.

Como puede imaginar, el flujo de aplicación típico va desde el código en la interfaz de usuario, a través del núcleo de la aplicación hasta el código de infraestructura, de regreso al núcleo de la aplicación y finalmente entrega una respuesta a la interfaz de usuario.

Herramientas

Lejos del código más importante de nuestro sistema, el núcleo de la aplicación, tenemos las herramientas que utiliza nuestra aplicación, por ejemplo, un motor de base de datos, un motor de búsqueda, un servidor web o una consola CLI (aunque las dos últimas también son de entrega mecanismos).

Si bien puede parecer extraño colocar una consola CLI en el mismo "depósito" que un motor de base de datos, y aunque tienen diferentes tipos de propósitos, de hecho son herramientas que utiliza la aplicación. La diferencia clave es que, mientras que la consola CLI y el servidor web se utilizan para decirle a nuestra aplicación que haga algo, nuestra aplicación le dice al motor de base de datos que haga algo. Esta es una distinción muy relevante, ya que tiene fuertes implicaciones sobre cómo construimos el código que conecta esas herramientas con el núcleo de la aplicación.

Conectando las herramientas y los mecanismos de entrega al núcleo de la aplicación

Las unidades de código que conectan las herramientas al núcleo de la aplicación se denominan adaptadores (Arquitectura de puertos y adaptadores). Los adaptadores son los que implementan efectivamente el código que permitirá que la lógica empresarial se comunique con una herramienta específica y viceversa.

Los adaptadores que le dicen a nuestra aplicación que haga algo se llaman Adaptadores Primarios o Conductores, mientras que los que nuestra aplicación les dice que hagan algo se llaman Adaptadores Secundarios o Conducidos.

Puertos

Sin embargo, estos adaptadores no se crean de forma aleatoria. Se crean para adaptarse a un punto de entrada muy específico al núcleo de la aplicación, un puerto. Un puerto no es más que una especificación de cómo la herramienta puede usar el núcleo de la aplicación, o cómo lo usa el núcleo de la aplicación. En la mayoría de los lenguajes y en su forma más simple, esta especificación, el puerto, será una interfaz, pero en realidad podría estar compuesta por varias interfaces y DTO.

Es importante tener en cuenta que los puertos (interfaces) pertenecen a la lógica empresarial, mientras que los adaptadores pertenecen al exterior. Para que este patrón funcione como debería, es de suma importancia que los puertos se creen para adaptarse a las necesidades del núcleo de la aplicación y no simplemente imitar las API de las herramientas.

Adaptadores primarios o impulsores

Los adaptadores primarios o de controlador envuelven un puerto y lo utilizan para indicarle al núcleo de la aplicación lo que debe hacer. Traducen todo lo que proviene de un mecanismo de entrega en una llamada de método en el Núcleo de la aplicación.

En otras palabras, nuestros Driving Adapters son Controladores o Comandos de Consola que son inyectados en su constructor con algún objeto cuya clase implementa la interfaz (Puerto) que requiere el controlador o comando de consola.

En un ejemplo más concreto, un puerto puede ser una interfaz de servicio o una interfaz de repositorio que requiere un controlador. La implementación concreta del Servicio, Repositorio o Consulta se inyecta y utiliza en el Controlador.

Alternativamente, un puerto puede ser una interfaz de bus de comando o bus de consulta. En este caso, se inyecta una implementación concreta del Bus de Comando o Consulta en el Controlador, quien luego construye un Comando o Consulta y lo pasa al Bus correspondiente.

Adaptadores secundarios o controlados

A diferencia de los adaptadores de controladores, que envuelven un puerto, los adaptadores impulsados implementan un puerto, una interfaz y luego se inyectan en el núcleo de la aplicación, donde sea que se requiera el puerto (tipo sugerido).

Por ejemplo, supongamos que tenemos una aplicación ingenua que necesita conservar los datos. Entonces creamos una interfaz de persistencia que satisface sus necesidades, con un método para guardar una matriz de datos y un método para eliminar una línea en una tabla por su ID. A partir de entonces, donde sea que nuestra aplicación necesite guardar o borrar datos, requerirá en su constructor un objeto que implemente la interfaz de persistencia que definimos.

Ahora creamos un adaptador específico para MySQL que implementará esa interfaz. Tendrá los métodos para guardar una matriz y eliminar una línea en una tabla, y la inyectaremos donde sea que se requiera la interfaz de persistencia.

Si en algún momento decidimos cambiar el proveedor de la base de datos, digamos PostgreSQL o MongoDB, solo necesitamos crear un nuevo adaptador que implemente la interfaz de persistencia y sea específico de PostgreSQL, e inyectar el nuevo adaptador en lugar del anterior.

Inversión de control

Una característica a tener en cuenta sobre este patrón es que los adaptadores dependen de una herramienta específica y un puerto específico (mediante la implementación de una interfaz). Pero nuestra lógica empresarial solo depende del puerto (interfaz), que está diseñado para adaptarse a las necesidades de la lógica empresarial, por lo que no depende de un adaptador o herramienta específicos.

Esto significa que la dirección de las dependencias es hacia el centro, es el principio de inversión de control a nivel arquitectónico.

Aunque, nuevamente, es de suma importancia que los puertos se creen para satisfacer las necesidades del núcleo de la aplicación y no simplemente imitar las API de las herramientas.

Organización del núcleo de la aplicación

La Arquitectura Onion recoge las capas DDD y las incorpora a la Arquitectura de Puertos y Adaptadores. Esas capas están destinadas a llevar algo de organización a la lógica empresarial, el interior del “hexágono” de Puertos y Adaptadores, y al igual que en Puertos y Adaptadores, la dirección de las dependencias es hacia el centro.

Capa de aplicación

Los casos de uso son los procesos que pueden ser activados en nuestro Núcleo de Aplicación por una o varias Interfaces de Usuario en nuestra aplicación. Por ejemplo, en un CMS podríamos tener la interfaz de usuario de la aplicación real utilizada por los usuarios comunes, otra interfaz de usuario independiente para los administradores de CMS, otra interfaz de usuario CLI y una API web. Estas IU (aplicaciones) podrían desencadenar casos de uso que pueden ser específicos de uno de ellos o reutilizados por varios de ellos.

Los casos de uso se definen en la capa de aplicación, la primera capa proporcionada por DDD y utilizada por la arquitectura Onion.

Esta capa contiene servicios de aplicación (y sus interfaces) como ciudadanos de primera clase, pero también contiene las interfaces de puertos y adaptadores (puertos) que incluyen interfaces ORM, interfaces de motores de búsqueda, interfaces de mensajería, etc. En el caso de que estemos usando un Command Bus y/o un Query Bus, esta capa es donde pertenecen los respectivos Handlers para los Comandos y Consultas.

Los servicios de aplicación y/o los controladores de comandos contienen la lógica para desarrollar un caso de uso, un proceso comercial. Normalmente, su función es:

  1. utilizar un repositorio para encontrar una o varias entidades;
  2. decirle a esas entidades que hagan algo de lógica de dominio;
  3. y usar el repositorio para conservar las entidades nuevamente, guardando efectivamente los cambios de datos.

Los controladores de comandos se pueden utilizar de dos formas diferentes:

  1. Pueden contener la lógica real para realizar el caso de uso;
  2. Pueden usarse como simples piezas de cableado en nuestra arquitectura, recibiendo un comando y simplemente activando la lógica que existe en un servicio de aplicación.

El enfoque a utilizar depende del contexto, por ejemplo:

  • ¿Ya tenemos los servicios de aplicaciones en su lugar y ahora estamos agregando un bus de comando?
  • ¿El Command Bus permite especificar cualquier clase/método como controlador, o necesitan extender o implementar clases o interfaces existentes?

Esta capa también contiene la activación de eventos de aplicación, que representan algún resultado de un caso de uso. Estos eventos desencadenan una lógica que es un efecto secundario de un caso de uso, como enviar correos electrónicos, notificar a una API de terceros, enviar una notificación de inserción o incluso iniciar otro caso de uso que pertenece a un componente diferente de la aplicación.

Capa de dominio

Más adentro, tenemos la capa de dominio. Los objetos en esta capa contienen los datos y la lógica para manipular esos datos, que es específico del dominio en sí y es independiente de los procesos comerciales que desencadenan esa lógica, son independientes y desconocen por completo la capa de aplicación.

Servicios de dominio

Como mencioné anteriormente, la función de un servicio de aplicación es:

  1. utilizar un repositorio para encontrar una o varias entidades;
  2. decirle a esas entidades que hagan algo de lógica de dominio;
  3. y usar el repositorio para conservar las entidades nuevamente, guardando efectivamente los cambios de datos.

Sin embargo, a veces nos encontramos con alguna lógica de dominio que involucra a diferentes entidades, del mismo tipo o no, y sentimos que esa lógica de dominio no pertenece a las entidades mismas, sentimos que esa lógica no es su responsabilidad directa.

Entonces, nuestra primera reacción podría ser colocar esa lógica fuera de las entidades, en un Servicio de aplicación. Sin embargo, esto significa que esa lógica de dominio no será reutilizable en otros casos de uso: ¡la lógica de dominio debe permanecer fuera de la capa de aplicación!

La solución es crear un Servicio de dominio, que tiene la función de recibir un conjunto de entidades y realizar alguna lógica empresarial en ellas. Un servicio de dominio pertenece a la capa de dominio y, por lo tanto, no sabe nada sobre las clases de la capa de aplicación, como los servicios de aplicación o los repositorios. Por otro lado, puede utilizar otros servicios de dominio y, por supuesto, los objetos del modelo de dominio.

Modelo de dominio

En el mismo centro, sin depender de nada fuera de él, está el Modelo de dominio, que contiene los objetos comerciales que representan algo en el dominio. Ejemplos de estos objetos son, en primer lugar, entidades, pero también objetos de valor, enumeraciones y cualquier objeto utilizado en el modelo de dominio.

El modelo de dominio también es donde “viven” los eventos de dominio. Estos eventos se activan cuando un conjunto específico de datos cambia y llevan esos cambios con ellos. En otras palabras, cuando una entidad cambia, se desencadena un evento de dominio y lleva los nuevos valores de las propiedades cambiadas. Estos eventos son perfectos, por ejemplo, para ser utilizados en Event Sourcing.

Componentes

Hasta ahora hemos estado segregando el código basado en capas, pero esa es la segregación de código de grano fino. La segregación de código de grano grueso es al menos tan importante y se trata de segregar el código de acuerdo con subdominios y contextos limitados, siguiendo las ideas de Robert C. Martin expresadas en una screaming arquitecture. Esto a menudo se conoce como "Paquete por función" o "Paquete por componente" en lugar de "Paquete por capa", y Simon Brown lo explica bastante bien en su publicación de blog "Paquete por componente y pruebas alineadas con la arquitectura":

 

 


 Soy un defensor del enfoque de "Paquete por componente" y, siguiendo el diagrama de Simon Brown sobre Paquete por componente, lo cambiaría descaradamente a lo siguiente:

 Estas secciones de código son transversales a las capas descritas anteriormente, son los componentes de nuestra aplicación. Los ejemplos de componentes pueden ser Autenticación, Autorización, Facturación, Usuario, Revisión o Cuenta, pero siempre están relacionados con el dominio. Los contextos limitados como Autorización y/o Autenticación deben verse como herramientas externas para las cuales creamos un adaptador y nos escondemos detrás de algún tipo de puerto.

Desacoplando los componentes

Al igual que las unidades de código de grano fino (clases, interfaces, rasgos, mixins,…), también las unidades de código de grano grueso (componentes) se benefician de un bajo acoplamiento y una alta cohesión.

Para desacoplar clases, hacemos uso de Dependency Injection, inyectando dependencias en una clase en lugar de instanciarlas dentro de la clase, y Dependency Inversion, al hacer que la clase dependa de abstracciones (interfaces y/o clases abstractas) en lugar de clases concretas. Esto significa que la clase dependiente no tiene conocimiento sobre la clase concreta que va a usar, no tiene ninguna referencia al nombre de clase totalmente calificado de las clases de las que depende.

De la misma manera, tener componentes completamente desacoplados significa que un componente no tiene conocimiento directo de ningún otro componente. En otras palabras, no hace referencia a ninguna unidad de código de grano fino de otro componente, ¡ni siquiera a interfaces! Esto significa que la inyección de dependencia y la inversión de dependencia no son suficientes para desacoplar componentes, necesitaremos algún tipo de construcciones arquitectónicas. ¡Podríamos necesitar eventos, un kernel compartido, consistencia eventual e incluso un servicio de descubrimiento!

Activando la lógica en otros componentes

Cuando uno de nuestros componentes (componente B) necesita hacer algo cada vez que sucede algo más en otro componente (componente A), no podemos simplemente hacer una llamada directa desde el componente A a una clase/método en el componente B porque entonces A estaría acoplado a B.

Sin embargo, podemos hacer que A use un despachador de eventos para enviar un evento de aplicación que se entregará a cualquier componente que lo escuche, incluido B, y el detector de eventos en B desencadenará la acción deseada. Esto significa que el componente A dependerá de un despachador de eventos, pero estará desacoplado de B.

Sin embargo, si el evento en sí "vive" en A, esto significa que B sabe acerca de la existencia de A, está acoplado a A. Para eliminar esta dependencia, podemos crear una biblioteca con un conjunto de funciones centrales de la aplicación que se compartirán entre todos los componentes, el núcleo compartido. Esto significa que ambos componentes dependerán del núcleo compartido, pero estarán desacoplados entre sí. El Kernel compartido contendrá funcionalidades como eventos de aplicación y dominio, pero también puede contener objetos de especificación y cualquier cosa que tenga sentido compartir, teniendo en cuenta que debe ser lo mínimo posible porque cualquier cambio en el Kernel compartido afectará a todos los componentes de la aplicación. Además, si tenemos un sistema políglota, digamos un ecosistema de microservicios donde están escritos en diferentes idiomas, el Kernel compartido debe ser independiente del idioma para que pueda ser entendido por todos los componentes, sea cual sea el idioma en el que se hayan escrito. . Por ejemplo, en lugar del núcleo compartido que contiene una clase de evento, contendrá la descripción del evento (es decir, nombre, propiedades, tal vez incluso métodos, aunque estos serían más útiles en un objeto de especificación) en un lenguaje agnóstico como JSON, de modo que todos los componentes/microservicios pueden interpretarlo y tal vez incluso generar automáticamente sus propias implementaciones concretas. Lea más sobre esto en mi publicación de seguimiento: Más que capas concéntricas.

Este enfoque funciona tanto en aplicaciones monolíticas como en aplicaciones distribuidas como ecosistemas de microservicios. Sin embargo, cuando los eventos solo se pueden entregar de forma asincrónica, para contextos donde la lógica de activación en otros componentes debe realizarse de inmediato, ¡este enfoque no será suficiente! El componente A necesitará hacer una llamada HTTP directa al componente B. En este caso, para tener los componentes desacoplados, necesitaremos un servicio de descubrimiento al cual A preguntará dónde debe enviar la solicitud para activar la acción deseada, o alternativamente hacer la solicitud al servicio de descubrimiento, que puede enviarla al servicio correspondiente y, finalmente, devolver una respuesta al solicitante. Este enfoque acoplará los componentes al servicio de descubrimiento, pero los mantendrá desacoplados entre sí.

Obtener datos de otros componentes

A mi modo de ver, a un componente no se le permite cambiar datos que no son "de su propiedad", pero está bien que consulte y utilice cualquier dato.

Almacenamiento de datos compartido entre componentes

Cuando un componente necesita usar datos que pertenecen a otro componente, digamos que un componente de facturación necesita usar el nombre de cliente que pertenece al componente de cuentas, el componente de facturación contendrá un objeto de consulta que consultará el almacenamiento de datos para esos datos. Esto simplemente significa que el componente de facturación puede conocer cualquier conjunto de datos, pero debe usar los datos que no "posee" como de solo lectura, por medio de consultas.

Almacenamiento de datos segregado por componente

En este caso, se aplica el mismo patrón, pero tenemos más complejidad a nivel de almacenamiento de datos. Tener componentes con su propio almacenamiento de datos significa que cada almacenamiento de datos contiene:

  • Un conjunto de datos que posee y es el único al que se le permite cambiar, lo que lo convierte en la única fuente de verdad;
  • Un conjunto de datos que es una copia de los datos de otros componentes, que no puede cambiar por sí solo, pero es necesario para la funcionalidad del componente y debe actualizarse cada vez que cambia en el componente propietario.

Cada componente creará una copia local de los datos que necesita de otros componentes, que se utilizará cuando sea necesario. Cuando los datos cambian en el componente propietario, ese componente propietario activará un evento de dominio que llevará los cambios de datos. Los componentes que tienen una copia de esos datos escucharán ese evento de dominio y actualizarán su copia local en consecuencia.

Flujo de control

Como dije anteriormente, el flujo de control va, por supuesto, del usuario al Núcleo de la aplicación, a las herramientas de infraestructura, de regreso al Núcleo de la aplicación y finalmente al usuario. Pero, ¿cómo encajan exactamente las clases? ¿Cuáles dependen de cuáles? ¿Cómo los componimos?

Siguiendo al tío Bob, en su artículo sobre Arquitectura limpia, intentaré explicar el flujo de control con diagramas "estilo" UML...

Sin bus de comando/consulta

En el caso de que no utilicemos un bus de comandos, los Controladores dependerán de un Servicio de Aplicación o de un Objeto de Consulta.

[EDICIÓN - 2017-11-18] Olvidé por completo el DTO que uso para devolver datos de la consulta, así que lo agregué ahora. Gracias a MorphineAdmintered quién me lo señaló.

En el diagrama anterior usamos una interfaz para el Servicio de aplicación, aunque podríamos argumentar que no es realmente necesario ya que el Servicio de aplicación es parte de nuestro código de aplicación y no queremos cambiarlo por otra implementación, aunque podríamos refactorizarlo. enteramente.

El objeto Consulta contendrá una consulta optimizada que simplemente devolverá algunos datos sin procesar para mostrarlos al usuario. Esos datos se devolverán en un DTO que se inyectará en un ViewModel. ThisViewModel puede tener alguna lógica de vista y se usará para completar una vista.

El Servicio de Aplicación, por otro lado, contendrá la lógica del caso de uso, la lógica que activaremos cuando queramos hacer algo en el sistema, en lugar de simplemente ver algunos datos. Los Servicios de Aplicación dependen de los Repositorios que devolverán la (s) Entidad (es) que contienen la lógica que debe activarse. También podría depender de un Servicio de dominio para coordinar un proceso de dominio en varias entidades, pero ese casi nunca es el caso.

Después de desplegar el caso de uso, es posible que el servicio de aplicaciones desee notificar a todo el sistema que ese caso de uso ha sucedido, en cuyo caso también dependerá de un despachador de eventos para desencadenar el evento.

Es interesante notar que colocamos interfaces tanto en el motor de persistencia como en los repositorios. Aunque pueda parecer redundante, sirven para diferentes propósitos:

  • La interfaz de persistencia es una capa de abstracción sobre el ORM, por lo que podemos intercambiar el ORM que se está utilizando sin cambios en el núcleo de la aplicación.
  • La interfaz del repositorio es una abstracción del motor de persistencia en sí. Supongamos que queremos cambiar de MySQL a MongoDB. La interfaz de persistencia puede ser la misma y, si queremos seguir usando el mismo ORM, incluso el adaptador de persistencia seguirá siendo el mismo. Sin embargo, el lenguaje de consulta es completamente diferente, por lo que podemos crear nuevos repositorios que usen el mismo mecanismo de persistencia, implementar las mismas interfaces de repositorio pero construir las consultas usando el lenguaje de consulta MongoDB en lugar de SQL.

Con un bus de comando/consulta

En el caso de que nuestra aplicación use un Bus de Comando/Consulta, el diagrama permanece prácticamente igual, con la excepción de que el controlador ahora depende del Bus y de un comando o Consulta. Instalará el comando o la consulta y se lo pasará al bus, quien encontrará el controlador adecuado para recibir y administrar el comando.

En el diagrama siguiente, el controlador de comandos utiliza un servicio de aplicación. Sin embargo, eso no siempre es necesario, de hecho, en la mayoría de los casos, el controlador contendrá toda la lógica del caso de uso. Solo necesitamos extraer la lógica del controlador en un Servicio de aplicación separado si necesitamos reutilizar esa misma lógica en otro controlador.

Es posible que hayas notado que no hay dependencia entre el bus y el comando, la consulta ni los controladores. Esto se debe a que, de hecho, deberían desconocerse entre sí para proporcionar un buen desacoplamiento. La forma en que el Bus sabrá qué Handler debe manejar qué Comando, o Consulta, debe configurarse con una mera configuración.

Como puede ver, en ambos casos todas las flechas, las dependencias, que cruzan el borde del núcleo de la aplicación, apuntan hacia adentro. Como se explicó anteriormente, esta es una regla fundamental de la arquitectura de puertos y adaptadores, la arquitectura de cebolla y la arquitectura limpia.

Conclusión

El objetivo, como siempre, es tener una base de código débilmente acoplada y altamente cohesiva, de modo que los cambios sean fáciles, rápidos y seguros de realizar.

Los planes no valen nada, pero la planificación lo es todo.

Eisenhower

Esta infografía es un mapa conceptual. Conocer y comprender todos estos conceptos nos ayudará a planificar una arquitectura saludable, una aplicación saludable.

Sin embargo:

El mapa no es el territorio.

Alfred Korzybski

¡Lo que significa que estas son solo pautas! La aplicación es el territorio, la realidad, el caso de uso concreto donde necesitamos aplicar nuestro conocimiento, ¡y eso es lo que definirá cómo se verá la arquitectura real!

Necesitamos comprender todos estos patrones, pero también siempre debemos pensar y comprender exactamente qué necesita nuestra aplicación, hasta dónde debemos llegar en aras del desacoplamiento y la cohesión
. Esta decisión puede depender de muchos factores, comenzando con los requisitos funcionales del proyecto, pero también puede incluir factores como el marco de tiempo para construir la aplicación, la vida útil de la aplicación, la experiencia del equipo de desarrollo, etc.

Así es, así es como le doy sentido a todo. Así es como lo racionalizo en mi cabeza.

Amplié estas ideas un poco más en una publicación de seguimiento: Más que capas concéntricas.

Sin embargo, ¿cómo hacemos todo esto explícito en el código base? Ese es el tema de una de mis próximas publicaciones: cómo reflejar la arquitectura y el dominio en el código.

Por último, pero no menos importante, gracias a mi colega Francesco Mastrogiacomo, por ayudarme a hacer que mi infografía se vea bien.


Continúa leyendo

Cómo redactar un Resume para IT (y otras posiciones)

De qué hablaremos Sí, hay muchos artículos sobre este tema, y muy buenos. Incluso hay portales que analizan tu resume y te hacen una devolución con recomendaciones de mejora; algunos funcionan con IA, otros con humanos detrás. En fin, aquí va mi versión. A los efectos de este artículo, usaré los términos resume y CV de manera indistinta; a pesar de que el primero se considera una versión más corta del primero (y dirigida a la posición a la que quieres aplicar), la realidad es que hoy en día existe una mayor aceptación de éste y por ello cuando te soliciten tu CV y tú envíes tu resume , nadie se rasgará las vestiduras... todo lo contrario. Si bien he tenido la dicha de tener diferentes experiencias a lo largo del tiempo (sector público y privado, contratado y freelance, para empresas locales y del extranjero, y a su vez más de un proyecto en cada espacio de trabajo) volcar toda esta información en mi resume/CV fue todo un proyecto en sí mismo, el cual involucró una ardua investigaci

Autocannon: probando el rendimiento de nuestro servicio web

Autocannon es una librería de benchmarking inspirada en wrk y escrita en Javascript, la cual nos permite probar la carga de nuestro sitio web o API.   Instalación y uso Debemos tener instalado node en nuestro equipo. Luego basta con instalar la librería de forma global para disponer de la misma para cualquier prueba: npm i autocannon -g Sólo con ello ya podremos ejecutar una prueba, con un comando similar al siguiente: autocannon -c 100 -d 5 -p 10 http://localhost:3000 En el comando anterior estamos indicando los siguientes parámetros: -c: cantidad de conexiones. -d: la cantidad de segundos que se ejecutará la prueba (duración). -p: la cantidad de solicitudes canalizadas (pipelined requests). La URL y puerto del servicio que deseamos probar Como resultado, veremos un reporte similar al siguiente: Running 10s test @ http://localhost:3000 100 connections with 10 pipelining factor ┌─────────┬───────┬────────┬────────┬────────┬───────────┬──────────┬────────┐ │ Stat │ 2.5% │ 50%