La Arquitectura Hexagonal, también conocida como Arquitectura de Puertos y Adaptadores, es una de las arquitecturas mas utilizadas hoy en dia. En este artículo se pretende enumerar las características propias de este tipo de arquitectura, aplicando arquitectura hexagonal en un pequeño ejemplo práctico.

 

Aplicando Arquitectura Hexagonal: principios

La Arquitectura Hexagonal es un tipo de arquitectura que se basa en el principio de Puertos y Adaptadores. Esta arquitectura sigue la premisa de garantizar la abstracción total del dominio respecto de todas las dependencias que podamos tener (repositorios, frameworks, librerías de terceros, etc.)

El principio de Puertos y Adaptadores, como su propio nombre indica, se basa en aislar completamente nuestro dominio de cualquier dependencia externa y manejar la interacción de estas dependencias con nuestro domino mediante el uso de:

  • Puertos: que nos permitan estandarizar las funcionalidades/acciones que utilizamos de dichas dependencias y de que manera o con qué estructuras de datos lo harán.
  • Adaptadores: que se encargarán de realizar la adaptación de las dependencias hacia las estructuras de datos que necesitemos dentro de nuestro dominio.

Además de estos principios básicos, la arquitectura hexagonal sigue los principios definidos por Robert C. Martin en The Clean Architecture para garantizar una arquitectura limpia, escalable y adaptable al cambio:

  • Independencia del framework: Tal y como hemos definido anteriormente, la arquitectura hexagonal nos garantiza independencia total de cualquier framework o dependencia externa.
  • Testeable: Al tener las reglas de negocio aisladas en nuestro dominio, podemos probar de manera unitaria todos los requerimientos de nuestra aplicación sin ningún factor externo que pueda adulterar la funcionalidad de éste.
  • Independiente de la UI: La interfaz de usuario es un ecosistema continuamente cambiante, lo cual no significa que las reglas de negocio lo hagan por igual. Abstaernos de la UI nos permite mantener un dominio más estable y que nos evite tener que entrar a modificarlo por factores completamente externos a las reglas de negocio.
  • Independiente de la base de datos: Nuestro dominio no debe conocer la manera en la que decidimos estructurar o guardar nuestros datos en un repositorio. Eso a él no le aporta nada, salvo acoplamiento y un futuro problema si decidimos cambiar nuestra base de datos en algun momento.
  • Independiente de cualquier agente externo: Nuestro dominio es una representación de nuestras reglas de negocio, por lo tanto no le concierne el conocimiento de cualquier dependencia externa. Cada librería externa tendrá su propio adaptador, que será quien se encargue de la interacción con el dominio.

La Architectura Hexagonal forma parte de las llamadas Clean Architectures, que se basan en el siguiente esquema:

hexagonalEsquema de Arquitectura Hexagonal. Extraida de Novoda

 

Podemos observar que los conceptos explicados anteriormente encajan en este esquema perfectamente. Y además de aplicar estos principios, en nuestro ejemplo práctico vamos a intentar siempre aplicar los principios SOLID, de la mejor manera posible. Si deseas conocer más acerca de la Arquitectura Hexagonal puedes encontrar más información en el blog de ApiumHub

 

Ejercicio Práctico: aplicando arquitectura hexagonal

Vamos a realizar un sencillo ejercicio que nos permita ver de manera práctica, la aplicación de los connceptos nombrados anteriormente. Imaginemos que tenemos que desarrollar una aplicación para un almacén en PHP 7 utilizando Symfony 3. En este primer articulo vamos a desarrollar la introducción de productos en el sistema.

Para ello vamos a desarrollar un punto de acceso tipo POST que definiremos como “/products”.

Como podemos ver, el enunciado ya nos puede dar unas pistas de las dependencias externas más claras que vamos a necesitar:

  • Symfony, como framework de la aplicación
  • Una base de datos para almacenar los productos introducidos: En este caso utilizaremos Doctrine como ORM para interactuar con un MySQL

En este ejercicio vamos a aplicar una estrategia inside-out, por lo tanto, comencemos por definir nuestro dominio. Vamos a implementar una entidad Product muy básica con cuatro campos: id, nombre, referencia y fecha de creación.

Para asegurar que los datos que van a pasar por nuestro dominio son los esperados, hemos declarado nuestra entidad con un constructor privado y solo podremos instanciar nuestra entidad mediante la función estática fromDto que espera una estructura de datos concreta basada en un Data Transfer Object que hemos definido como CreateProductRequestDto, tal y como podemos observar a continuación:


class Product
 {
     private $id;
 
     private $name;
 
     private $reference;
 
     private $createdAt;
 
     private function __construct(
         string $name,
         string $reference
     )
     {
         $this->name = $name;
         $this->reference = $reference;
         $this->createdAt = new \DateTime();
     }
 
     public static function fromDto(CreateProductRequestDto $createProductResponseDto): Product
     {
         return new Product(
             $createProductResponseDto->name(),
             $createProductResponseDto->reference()
         );
     }
 
     public function toDto()
     {
         return new CreateProductResponseDto(
             $this->id,
             $this->name, $this->reference, $this->createdAt
         );
     }
 }

Como podemos observar, los parametros de nuestra entidad son privados, y solo serán accesibles al exterior mediante la estructura de datos definida como CreateProductResponseDto, asegurandonos así de que solo vamos a exponer aquellos datos que realmente deseamos mostrar. Con esta metodología hemos querido ir un paso mas allá, y vamos a diferenciar entre la capa más puramente denominada como dominio y la capa de aplicación, que a menudo tienden a mezclarse, y solo vamos a permitir la interacción con nuestro dominio, si la estructura de datos es la adecuada.

A continuación, procederemos a implementar la interacción de una base de datos con nuestro dominio. Para ello, necesitaremos definir un puerto que defina las funcionalidades que podrá realizar nuestro dominio con la dependencia externa y un adaptador que implemente la relación entre la dependencia externa y nuestro dominio.

En este caso, como puerto definiremos una interfaz de repositorio en la cual vamos a definir los metodos que vamos a necesitar desde nuestro dominio. Para ello hemos creado la clase ProductRepositoryInterface, que realizará la función de puerto:


interface ProductRepositoryInterface
{
    public function find(string $id): Product;

    public function save (Product $product): void;
}


Y como adaptador hemos creado la clase ProductRepositoryDoctrineAdapter que implementará las funciones definidas en la interfaz anterior. Estas serán funciones impuras, puesto que interactuan directamente con la dependencia externa y adaptan los datos para poder ser usados en nuestro dominio, tal que así:


class ProductRepositoryDoctrineAdapter implements ProductRepositoryInterface
{
    /** @var EntityRepository */
    private $productRepository;

    /** @var EntityManager */
    private $entityManager;

    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->productRepository = $entityManager->getRepository(Product::class);;
    }

    public function find(string $id): Product
    {
        $this->productRepository->find($id);
    }

    public function save(Product $product): void
    {
        $this->entityManager->persist($product);
        $this->entityManager->flush();
    }
}

En este caso, podemos ver como se inyecta en el constructor la dependencia externa, y como se implementan las funciones definidas en la interfaz que usamos como puerto.

Ahora vamos a ver, como va a interaccionar todo esto con nuestro dominio. Para ello, hemos creado una capa de aplicación, en la cual tendremos las acciones que puede realizar nuestro sistema. En esta capa además, será donde introduciremos las llamadas a los adaptadores, a los cuales tendremos acceso mediante inyección de dependencias:


class ProductApplicationService
{
    private $productRepository;

    public function __construct(ProductRepositoryInterface $productRepository)
    {
        $this->productRepository = $productRepository;
    }

    (...)
}

Como podemos observar, en el constructor hemos definido el puerto ProductRepositoryInterface y no el adaptador, ¿Por qué? Muy sencillo, si en el futuro en lugar de utilizar Doctrine con MySql queremos simplemente persistir nuestros datos en una cache de Redis, por ejemplo, sería tan sencillo como crear un nuevo adapter que cumpla con el contrato definido en ProductRepositoryInterface y cambiar la inyección de la dependencia que hemos definido en un fichero yml:

services:
  product.application.product_service:
    class: ProductBundle\Application\ProductApplicationService
    arguments: ['@product.repository.product.doctrine_adapter']

  product.repository.product.doctrine_adapter:
    class: ProductBundle\Repository\ProductRepositoryDoctrineAdapter
    arguments: ['@doctrine.orm.entity_manager']

Será tan sencillo como declarar el nuevo adapter, y substituir la dependencia del servicio aplicativo. Esto nos demuestra que hemos construido un dominio totalmente agnostico de sus dependencias, puesto que no tendremos que tocar una sola línea de código productivo para cambiar la dependencia.

Ahora veamos las utilización del adaptador en el servicio aplicativo:

class ProductApplicationService
{
    (...)

    public function createProduct(CreateProductRequestDto $createProductRequestDto): CreateProductResponseDto
        {
            $product = Product::fromDto($createProductRequestDto);
    
            $this->productRepository->save($product);
    
            return $product->toDto();
        }
}

Como se puede observar, en el servicio aplicativo estamos realizando la orquestación entre el dominio puro (la creación de una entidad de dominio, basada en la estructura de datos CreateProductRequestDto) y la llamada al adaptador para realizar la persistencia del objeto creado. De esta manera garantizamos que la capa de dominio nunca llegue a estar contaminada ni llegue a interactuar con adaptadores de dependencias externas, centrándose así unicamente en las reglas de negocio que tenga la aplicación.

Como último apunte y tal y como habréis podido observar con anterioridad, hemos creado dos Data Transfer Objects(DTOs): CreateProductRequestDto y CreateProductResponseDto. Esto es para garantizar la correcta estructura de los datos de entrada y salida de nuestro dominio. Si forzamos a que desde el controlador se nos envíe un DTO concreto, estamos abstrayéndonos de como se están enviando los datos a nuestra aplicación y garantizando que entran en nuestro dominio con la estructura de datos adecuada. Lo mismo sucede con el CreateProductResponseDto, que nos asegura una forma centralizada de decidir que datos exponemos de nuestro dominio al exterior.

Si intentamos extrapolar este ejercicio práctico a la imagen que aparece en la sección anterior, podemos ver como la definición de nuestras capas encaja perfectamente en el esquema teórico:

Estructura de nuestro ejemplo aplicado al esquema de Arquitectura Hexagonal.

 

Conclusiones: aplicando arquitectura hexagonal

Repasando los conceptos que hemos enumerado en la primera parte de nuestro artículo conjuntamente con el ejemplo desarrollado, podemos apreciar la aplicación de dichos conceptos en nuestra aplicación:

  • Se ha abstraido totalmente la persistencia de datos de nuestro dominio
  • Se ha implementado un ejemplo de un puerto y un adaptador satisfactoriamente
  • Se han implementado DTOs para ver un ejemplo de como transferir los datos entre las diferentes capas de abstracción ( Controlador y capa de Aplicación, que separa nuestro dominio del framework).
  • La separación por capas nos brinda un sistema facilmente testeable de forma unitaria, permitiendo mockear las dependencias de nuestro servicio aplicativo.
  • Se ha creado un dominio totalmente agnóstico de las dependencias, del framework y de la UI.

Soy consciente de que este ha sido un ejemplo muy simple, pero quería enfatizar en el desarrollo de la arquitectura sin perdernos en los detalles de implementación de la aplicación en sí misma. Nos tenemos que quedar con las teorias aplicadas en este ejercicio y lo que nos aportan en nuestro dia a día como desarrolladores, ayudándonos a implementar código limpio, mantenible y fácilmente testeable.

 

Si queréis tener más información sobre como seguir aplicando arquitectura hexagonal, no olvidéis de subscribiros a nuestro newsletter mensual aquí

 

Si te gustó este artículo “aplicando arquitectura hexagonal”, te puede gustar: 

 

Barcelona como ciudad intelignete

Mapa de los “main players”: ecosistema startup y tech en Barcelona

Proyectos IoT que cambiarán el mundo 

Ecosistema de salud digital en Barcelona

Cluster de salud digital  

Inovación disruptiva: ejemplos 

Búsqueda visual en el comercio electrónico