Las traducciones son generadas a través de traducción automática. En caso de conflicto entre la traducción y la version original de inglés, prevalecerá la version en inglés.
Patrón de arquitectura hexagonal
Intención
En 2005, el Dr. Alistair Cockburn propuso el patrón de arquitectura hexagonal, también denominado patrón de puertos y adaptadores. Su objetivo es crear arquitecturas de acoplamiento débil en las que los componentes de las aplicaciones se puedan probar de manera independiente, sin depender de los almacenes de datos ni de las interfaces de usuario (UI). Este patrón ayuda a evitar el bloqueo tecnológico de los almacenes de datos y las interfaces de usuario. Esto facilita cambiar el conjunto de tecnologías a lo largo del tiempo, con un impacto limitado o nulo en la lógica empresarial. En esta arquitectura de acoplamiento débil, la aplicación se comunica con los componentes externos a través de interfaces denominadas puertos y utiliza adaptadores para traducir los intercambios técnicos con estos componentes.
Motivación
El patrón de arquitectura hexagonal se utiliza para aislar la lógica empresarial (lógica de dominio) del código de infraestructura relacionado, como el código para acceder a una base de datos o a las API externas. Este patrón resulta útil para crear una lógica empresarial y un código de infraestructura acoplados de manera débil para las funciones AWS Lambda que requieren la integración con servicios externos. En las arquitecturas tradicionales, una práctica habitual es integrar la lógica empresarial en la capa de bases de datos como procedimientos almacenados y en la interfaz de usuario. Esta práctica, junto con el uso de constructos específicos de la interfaz de usuario en la lógica empresarial, da como resultado arquitecturas de acoplamiento ajustado que provocan cuellos de botella en las migraciones de bases de datos y en los esfuerzos de modernización de la experiencia de usuario (UX). El patrón de arquitectura hexagonal le permite diseñar los sistemas y aplicaciones según la finalidad propósito y no según la tecnología. Esta estrategia da como resultado componentes de aplicaciones que se pueden intercambiar de manera fácil, como bases de datos, experiencia de usuario y componentes de servicio.
Aplicabilidad
Utilice el patrón de arquitectura hexagonal cuando suceda lo siguiente:
-
Desea desacoplar la arquitectura de la aplicación para crear componentes que puedan probarse por completo.
-
Varios tipos de clientes pueden utilizar la misma lógica de dominio.
-
En el caso de los componentes de la interfaz de usuario y la base de datos son necesarias actualizaciones tecnológicas periódicas que no afectan a la lógica de la aplicación.
-
Para la aplicación son necesarios varios proveedores de entrada y consumidores de salida, y la personalización de la lógica de la aplicación genera complejidad y falta de extensibilidad del código.
Problemas y consideraciones
-
Diseño basado en dominios: la arquitectura hexagonal funciona muy bien con el diseño basado en dominios (DDD). Cada componente de la aplicación representa un subdominio en la DDD, y las arquitecturas hexagonales se pueden utilizar para lograr un acoplamiento débil entre los componentes de la aplicación.
-
Capacidad de hacer pruebas. Desde el diseño, una arquitectura hexagonal utiliza abstracciones para las entradas y las salidas. Por lo tanto, resulta más fácil escribir las pruebas unitarias y las pruebas de manera aislada debido al acoplamiento débil inherente.
-
Complejidad. La complejidad de separar la lógica empresarial del código de infraestructura, si se gestiona con cuidado, puede aportar ventajas excelentes, como la agilidad, la cobertura de las pruebas y la adaptabilidad de la tecnología. De lo contrario, la solución de los problemas puede llegar a ser compleja.
-
Sobrecarga del mantenimiento. El código adaptador adicional que hace que la arquitectura sea conectable se justifica solo si el componente de la aplicación requiere varias fuentes de entrada y destinos de salida para escribir, o cuando el almacén de datos de entrada y salida tiene que cambiar con el tiempo. De lo contrario, el adaptador se convierte en una capa adicional que hay que mantener, lo que supone una sobrecarga de mantenimiento.
-
Problemas de latencia. El uso de puertos y adaptadores agrega otra capa, lo que puede provocar latencia.
Implementación
Las arquitecturas hexagonales permiten que la lógica empresarial y de las aplicaciones se aíslen del código de infraestructura y del código que integra la aplicación con las interfaces de usuario, las API externas, las bases de datos y los agentes de mensajes. Puede conectar de manera fácil los componentes de la lógica empresarial con otros componentes (como las bases de datos) de la arquitectura de la aplicación mediante puertos y adaptadores.
Los puertos son puntos de entrada independientes de la tecnología a un componente de la aplicación. Estas interfaces personalizadas determinan la interfaz que permite a los agentes externos comunicarse con el componente de la aplicación, sin importar quién o qué implemente la interfaz. Esto es similar a cómo permite un puerto USB que muchos tipos de dispositivos se comuniquen con un equipo, siempre que utilicen un adaptador USB.
Los adaptadores interactúan con la aplicación a través de un puerto con una tecnología específica. Los adaptadores se conectan a estos puertos, reciben datos de los puertos o los proporcionan y los transforman para su procesamiento posterior. Por ejemplo, un adaptador de REST permite a los agentes comunicarse con el componente de la aplicación a través de una API de REST. Un puerto puede tener varios adaptadores sin que esto suponga ningún riesgo para el puerto ni para el componente de la aplicación. Para ampliar el ejemplo anterior, agregar un adaptador de GraphQL al mismo puerto proporciona un medio más para que los agentes interactúen con la aplicación a través de una API de GraphQL sin afectar a la API de REST, el puerto o la aplicación.
Los puertos se conectan a la aplicación y los adaptadores sirven de conexión con el mundo exterior. Puede utilizar los puertos para crear componentes de aplicaciones con acoplamiento débil e intercambiar los componentes dependientes mediante un cambio de adaptador. Esto permite que el componente de la aplicación interactúe con entradas y salidas externas sin necesidad de conocer el contexto. Los componentes son intercambiables a cualquier nivel, lo que facilita las pruebas automatizadas. Puede probar los componentes de manera independiente sin depender del código de la infraestructura, en lugar de aprovisionar un entorno completo para hacer las pruebas. La lógica de la aplicación no depende de factores externos, por lo que se simplifican las pruebas y resulta más fácil simular las dependencias.
Por ejemplo, en una arquitectura con acoplamiento débil, un componente de la aplicación debe poder leer y escribir datos sin conocer los detalles del almacén de datos. La responsabilidad del componente de la aplicación es suministrar los datos a una interfaz (puerto). Un adaptador define la lógica de escritura en un almacén de datos, que puede ser una base de datos, un sistema de archivos o un sistema de almacenamiento de objetos como Amazon S3, según las necesidades de la aplicación.
Arquitectura de alto nivel
La aplicación o el componente de la aplicación contienen la lógica empresarial principal. Recibe comandos o consultas de los puertos y envía las solicitudes a través de los puertos a agentes externos, que se implementan mediante adaptadores, como se ilustra en el diagrama siguiente.
Implementación mediante Servicios de AWS
Las funciones de AWS Lambda suelen contener lógica empresarial y código de integración de bases de datos, que están acoplados de manera ajustada para cumplir un objetivo. Puede utilizar el patrón de arquitectura hexagonal para separar la lógica empresarial del código de infraestructura. Esta separación permite hacer pruebas unitarias de la lógica empresarial sin depender del código de la base de datos y mejora la agilidad del proceso de desarrollo.
En la arquitectura siguiente, una función de Lambda implementa el patrón de arquitectura hexagonal. La API de REST de Amazon API Gateway inicia la función de Lambda. La función implementa la lógica empresarial y escribe los datos en las tablas de DynamoDB.
Código de muestra
El código de ejemplo de esta sección muestra cómo implementar el modelo de dominio mediante Lambda, separarlo del código de infraestructura (como el código para acceder a DynamoDB) e implementar las pruebas unitarias para la función.
Modelo de dominio
La clase de modelo de dominio no conoce las dependencias ni los componentes externos, solo implementa la lógica empresarial. En el ejemplo siguiente, la clase Recipient es una clase de modelo de dominio que verifica si hay superposiciones en la fecha de reserva.
class Recipient: def __init__(self, recipient_id:str, email:str, first_name:str, last_name:str, age:int): self.__recipient_id = recipient_id self.__email = email self.__first_name = first_name self.__last_name = last_name self.__age = age self.__slots = [] @property def recipient_id(self): return self.__recipient_id #..... def are_slots_same_date(self, slot:Slot) -> bool: for selfslot in self.__slots: if selfslot.reservation_date == slot.reservation_date: return True return False def is_slot_counts_equal_or_over_two(self) -> bool: #.....
Puerto de entrada
La clase RecipientInputPort se conecta con la clase receptora y ejecuta la lógica del dominio.
class RecipientInputPort(IRecipientInputPort): def __init__(self, recipient_output_port: IRecipientOutputPort, slot_output_port: ISlotOutputPort): self.__recipient_output_port = recipient_output_port self.__slot_output_port = slot_output_port ''' make reservation: adapting domain model business logic ''' def make_reservation(self, recipient_id:str, slot_id:str) -> Status: status = None # --------------------------------------------------- # get an instance from output port # --------------------------------------------------- recipient = self.__recipient_output_port.get_recipient_by_id(recipient_id) slot = self.__slot_output_port.get_slot_by_id(slot_id) if recipient == None or slot == None: return Status(400, "Request instance is not found. Something wrong!") print(f"recipient: {recipient.first_name}, slot date: {slot.reservation_date}") # --------------------------------------------------- # execute domain logic # --------------------------------------------------- ret = recipient.add_reserve_slot(slot) # --------------------------------------------------- # persistent an instance throgh output port # --------------------------------------------------- if ret == True: ret = self.__recipient_output_port.add_reservation(recipient) if ret == True: status = Status(200, "The recipient's reservation is added.") else: status = Status(200, "The recipient's reservation is NOT added!") return status
Clase del adaptador de DynamoDB
La clase DDBRecipientAdapter implementa el acceso a las tablas de DynamoDB.
class DDBRecipientAdapter(IRecipientAdapter): def __init__(self): ddb = boto3.resource('dynamodb') self.__table = ddb.Table(table_name) def load(self, recipient_id:str) -> Recipient: try: response = self.__table.get_item( Key={'pk': pk_prefix + recipient_id}) ... def save(self, recipient:Recipient) -> bool: try: item = { "pk": pk_prefix + recipient.recipient_id, "email": recipient.email, "first_name": recipient.first_name, "last_name": recipient.last_name, "age": recipient.age, "slots": [] } # ...
La función de Lambda get_recipient_input_port es una fábrica de instancias de la clase RecipientInputPort. Construye instancias de las clases de puertos de salida con las instancias del adaptador relacionadas.
def get_recipient_input_port(): return RecipientInputPort( RecipientOutputPort(DDBRecipientAdapter()), SlotOutputPort(DDBSlotAdapter())) def lambda_handler(event, context): body = json.loads(event['body']) recipient_id = body['recipient_id'] slot_id = body['slot_id'] # get an input port instance recipient_input_port = get_recipient_input_port() status = recipient_input_port.make_reservation(recipient_id, slot_id) return { "statusCode": status.status_code, "body": json.dumps({ "message": status.message }), }
Pruebas unitarias
Puede probar la lógica empresarial de las clases de modelos de dominio mediante la inyección de clases simuladas. En el ejemplo siguiente se proporciona la prueba unitaria de la clase Recipent del modelo de dominio.
def test_add_slot_one(fixture_recipient, fixture_slot): slot = fixture_slot target = fixture_recipient target.add_reserve_slot(slot) assert slot != None assert target != None assert 1 == len(target.slots) assert slot.slot_id == target.slots[0].slot_id assert slot.reservation_date == target.slots[0].reservation_date assert slot.location == target.slots[0].location assert False == target.slots[0].is_vacant def test_add_slot_two(fixture_recipient, fixture_slot, fixture_slot_2): #..... def test_cannot_append_slot_more_than_two(fixture_recipient, fixture_slot, fixture_slot_2, fixture_slot_3): #..... def test_cannot_append_same_date_slot(fixture_recipient, fixture_slot): #.....
Repositorio GitHub
Para obtener una implementación completa de la arquitectura de ejemplo para este patrón, consulte el repositorio de GitHub en https://github.com/aws-samples/aws-lambda-domain-model-sample
Contenido relacionado
-
Hexagonal architecture
, artículo de Alistair Cockburn -
Developing evolutionary architectures with AWS Lambda
(AWS entrada del blog en japonés)
Videos
En el video siguiente (en japonés) se analiza el uso de la arquitectura hexagonal en la implementación de un modelo de dominio mediante una función de Lambda.