Arquitectura Hexagonal

2016-01-09

Cuando empecé a aprender arquitectura hexagonal no terminaba de entender la teoría y aún menos la práctica. También me pasó que mezclaba este concepto con DDD. Personalmente, en lo que es arquitectura hexagonal creo que resulta más eficaz empezar por la práctica. Después de varios meses trabajando con ella, la teoría empieza a encajar y por tanto, empiezo a interiorizar este tipo de arquitectura. En este post trataré de recoger lo que he aprendido sobre este tema gracias a otros posts, videos, práctica y debates con compañeros y mentores.

DEFINICIONES

Esta arquitectura también es conocida como ports & adapters architecture. Como yo definiría estos conceptos sería de la siguiente manera:

  • Puerto: Definición de una interfaz pública.
  • Adapter: Especialización de un puerto para un contexto concreto.
    Carlos nos explicó estos dos conceptos con una simple y gran metáfora. Imaginemos que tenemos el puerto USB de nuestro PC (puerto). En este puerto podemos introducir un ratón, un teclado, una impresora… (adapters).

OBJETIVO

El principal objetivo de la arquitectura hexagonal es separar nuestra aplicación en distintas capas que tienen su propia responsabilidad. De esta manera conseguimos desacoplar capas de nuestra aplicación permitiendo que evolucionen de manera aislada. Tener el sistema separado por responsabilidades nos facilitará la reutilización de capas.

Gracias a este desacoplamiento obtenemos también la ventaja de poder testear estas capas sin que intervengan otras externas (falseando el comportamiento con dobles de pruebas).

ARQUITECTURA HEXAGONAL

La capa más importante de nuestra aplicación es aquella que cubre nuestras reglas de negocio. Nuestras reglas de negocio no deben verse afectadas por cambios externos a su capa, por lo que debemos aislarlas. Nuestro negocio será una librería/biblioteca que otros proyectos podrán utilizar. Vayamos al ejemplo clásico:

Arquitectura hexagonal

Generalmente cuando hablamos del hexágono, nos referimos a nuestro negocio. Aquí podemos ver que tenemos un proyecto de persistencia y luego un controller y una aplicación de escritorio (personalmente me gusta pensar en el controller como algo externo al hexágono). Podríamos definir cada capa con su responsabilidad. El controller y la aplciación de escritorio son mecanismos de entrega para el usuarios. Mientras que nuestra persistencia, pues eso, persiste.

Cuando veía el dibujo y las implementaciones, me quedaba una duda. ¿Quiénes son aquí adapters y quiénes puertos? Veamos un ejemplo sencillo.

Supongamos que estamos en un punto del proyecto en el cual tenemos que guardar un usuario. Bien, el hecho de que un usuario tenga que persistirse es una regla de negocio. Queremos que los usuarios puedan loguearse en el sistema por lo tanto debemos guardarlos. No obstante, es el cómo lo que está completamente fuera de nuestro negocio. Es un nivel de detalle irrelevante para neustra lógica. Será nuestro módulo de persistencia quien defina distintas formas de persistir (adpaters). Teniendo esto en cuenta, en nuestro negocio podríamos definir una interfaz “UserRepository” que será nuestro puerto:

1
2
3
public interface UserRepository {
void SaveUser(User user);
}

Mientras que en nuestro módulo de persistencia, al tener como dependencia a Business, podemos hacer implementaciones de esta interfaz. Como ya vimos en el dibujo podrían ser SQL, mongo… Estas implementaciones serían nuestros adpaters.

1
2
3
4
5
public class SqlUserRepository : UserRepository {
public void SaveUser(User user) {
// Code to save the user in sql
}
}

Por tanto, dentro de nuestro negocio, quien quiera que use a UserRepository como colaborador podrá recibir una de estas implementaciones. Lo que nos permite mediante inyección de la dependencia, usar cualquiera. Haciendo que nuestro negocio sea independiente del cómo.

MÁS PUERTOS Y ADAPTADORES

En el apartado de definiciones, hice una breve explicación de lo que es un puerto y un adapter. No obstante, tanto los puertos como los adapters pueden ser primarios o secundarios.

  • Puerto primario: es el que representa a la API pública de nuestro dominio. Aquellas acciones que nuestra aplicación puede realizar.
  • Puerto secundario: es una interfaz que nuestro dominio utiliza internamente.
  • Adaptador primario: es un adaptador para los puertos primarios. Digamos que actúa de intermediario entre el usuario y nuestra lógica de negocio. Este adaptador gestiona el flujo de la aplicación.
  • Adaptador secundario: es un adaptador para un puerto secundario. Es una implementación para la interfaz definida como puerto primario. Esta implementación se le inyectará al hexágono para definir cuál de los adaptadores utilizará.
    Teniendo esto en cuenta, en el ejemplo descrito anteriormente con los repositorios estamos hablando de un puerto secundario (UserRepository) y un adaptador secundario (SqlUserRepository). Estos se están usando internamente en el dominio. Alguien inyectará el adaptador correspondiente.

Veamos un ejemplo de un puerto primario:

1
2
3
4
5
6
7
8
9
10
11
public class UserService {
private readonly UserRepository repository;
public UserService(UserRepository repository){
this.repository = repository;
}
public void CreateUser(User user){
repository.SaveUser(user);
}
}

Esto es un puerto primario de nuestro hexágono. En nuestro negocio se pueden crear usuarios y lo haremos mediante esta API pública. En el caso del dibujo, vemos que tenemos dos adapters para este puerto (ambos primarios): El controller y la aplicación de escritorio. En estos adaptadores se le inyectará a este servicio el adaptador secundario que necesita (alguien que cumpla la interfaz de UserRepository). Estos adaptadores primarios se encargarán de recoger inputs del usuario para pasársela a nuestro hexágono, este procesará ese input usando los adaptadores secundarios si fuese necesario. Una vez termine, devolverá una respuesta para que el adaptador pueda avisar al usuario.

Teniendo en cuenta los adaptadores que tenemos (Escritorio y controller). Cabe destacar que el hecho de que ambos respondan al mismo puerto no quiere decir que sean la misma aplicación. Simplemente quiere decir que ambos adaptadores tienen una lógica de negocio común que es la que contiene este hexágono (que tampoco quiere decir que sea la única en ambos casos).

CONCLUSIÓN

Personalmente me parece una manera muy acertada de organizar la arquitectura de un proyecto. Aunque he hablado solo de un hexágono, habrá ocasiones en las que una aplicación use lógica de dos hexágonos. Cada hexágono podrá evolucionar en su propio contexto.

Considero que hace que el sistema sea más fácil de testear ya que en muchos puntos podremos omitir detalles externos. Además nos permite empezar a desarrollar por las reglas de negocio pudiendo mockear nuestra persistencia (dejándola para el final). También me gusta que esta división por responsabilidades nos permite desarrollar en paralelo.

Por último, podemos ver que a medida que vamos entrando en el hexágono vamos encontrando un mayor nivel de detalle. Mientras que más cercano a los límites tenemos abstracciones y puertos. Esto nos facilita tener una visión global de cómo funciona nuestro sistema pudiéndonos abstraer de los detalles.

BIBLIOGRAFÍA