Dentro de C# utilizamos una gran cantidad de arquitecturas, diseños y patrones, una de las arquitecturas más populares es la arquitectura hexagonal, a la que también llamaremos puertos y adaptadores (ports and adapters) debido a su estructura.
1 - Qué es la arquitectura hexagonal?
Llamamos arquitectura hexagonal porque lo que tenemos es un núcleo central, donde tenemos la lógica de negocio, y luego alrededor del mismo tenemos otra capa que son los puertos y adaptadores que nos permiten interactuar con servicios externos, ya bien sean servicios de otras empresas, la base de datos, el sistema de ficheros o la propia interfaz.
Un apartado clave de la arquitectura hexagonal es que el núcleo, core (o llamalo como quieras), la lógica de negocio no sabe nada de cómo se procesan los datos o como se reciben los datos, pero todas las reglas y procesos están en un solo lugar. Lo que facilita el testeo y la simplicidad a la hora de desarrollar.
Ahora llega el momento de explicar los puertos y los adaptadores.
Podemos entender los puertos como interfaces, las cuales inyectamos en nuestra lógica de negocio, estas interfaces contienen el contrato que la lógica va a utilizar.
Por lo que un adaptador no es más que la abstracción no implementación de dicho puerto, siendo la implementación completamente transparente e irrelevante al caso de uso.
En mi opinión, usar los términos “puertos y adaptadores” es complicar las cosas cuando la realidad es que es interfaz e implementación. Pero bueno, cosas menores.
2 - Características de la arquitectura hexagonal
Como todo en programación tenemos beneficios e inconvenientes.
Desde mi punto de vista hay dos inconvenientes;
El primero es obvio, para implementar Hexagonal como es debido tienes que crear los puertos y los adaptadores, lo que lleva a tener mucho código, otro punto importante es que tienes que hacer separación entre Entidades y Dto de una forma muy clara.
Por ejemplo, yo he visto lo siguiente: Leemos un usuario de la BBDD con Entity Framework y ese usuario lo convertimos en un DTO, ese DTO lo modificamos, cambiamos un valor y lo volvemos a guardar, pero claro, estás mandando a tu abstracción (adapter) de la base de datos un DTO y no la entidad de Entity framework por lo que tienes que volver a leer la entidad para poder guardarla.
Además, esta empresa en particular devolvía el usuario actualizado al usuario como respuesta, lo que implica otra conversión (aunque se puede devolver una interfaz IUser).
Como ejemplo creo que ilustra muy bien a lo que me refiero, aquí hay claramente pasos “de más”, cuando este proceso en particular se puede hacer con 4 líneas de código.
El otro punto negativo, aunque depende más de la empresa en la que estés es que toda la lógica de negocio está ubicada en un solo fichero, crean un fichero llamado UsuarioRepository, y ahí está toda la lógica que tiene que ver con el usuario, el problema es que está en un solo fichero todas las acciones, por ejemplo, leer un usuario, actualizarlo, borrarlo, validarlo, etc, todo en el mismo fichero.
Nota: en este caso UsuarioRepository es la lógica del usuario, no el acceso a datos... Si, tema naming da para otro post.
Y eso es si está partido, que muchas veces la app entera tiene un solo fichero, sería AppRepository y todo lo que tiene que ver con usuarios ahí, si tu app va sobre libros, todo lo que tiene que ver con libros en ese mismo fichero, y al final acabas con un fichero de tres mil líneas que se puede partir muy rápido y de una forma simple pero nadie quiere hacerlo por si algo se rompe.
Si en vez de tenerlo todo en un único fichero, separamos los casos de uso, mejoraremos la aplicación de forma drástica, porque ya estamos separando la responsabilidad con los puertos y los adaptadores, así que por qué no separar los casos de uso también. Llegado el momento cuando otro desarrollador quiera hacer un cambio o implementar algo nuevo solo tiene que preocuparse por lo que está haciendo.
Si juntamos todo lo anterior, nos damos cuenta que hacer tests es muy sencillo, o bueno de primeras parece sencillo ya que todas las dependencias están detrás de interfaces, lo que implica que podemos hacer un doble sin problemas.
Testear hexagonal es muy simple siempre y cuando tengamos los casos de uso separados, ya que solo inyectamos los puertos (interfaces) que usamos en ese caso de uso, pero si tenemos un God Object estaremos inyectando muchos puertos que no necesitamos para cada uno de los casos de uso.