El diseño incremental es una forma de trabajar que nos va a permitir ir añadiendo nueva funcionalidad a nuestras aplicaciones de forma progresiva. Introducir estas nuevas funcionalidades puede llegar a ser un dolor enorme para el equipo, si esto es sinónimo de muchos cambios.
Para solucionar esta problemática existe un patrón de arquitectura denominado Ports and Adapters que nos permitirá introducir estos cambios de forma casi transparente.
Porque usar Ports & Adapters?
En las últimas semanas he estado trabajando en un proyecto personal en el que he aprovechado a aplicar muchas de las técnicas que utilizo diariamente en el trabajo. Este proyecto consiste en una app que te permite subscribirte a determinados autores, títulos de libros o palabras clave en general. Cuando aparecen nuevos libros para estos términos los usuarios son notificados.
Desde el primer momento tuve claro que quería crear la app de forma incremental, en pequeños pasos y de tal forma que añadir nuevos componentes a la misma fuera realmente sencillo. Para ello utilicé una arquitectura en capas conocida como Ports and Apdaters, también llamada Arquitectura Hexagonal, y que es la que usamos en el trabajo.
Este tipo de arquitectura, entre otras muchas ventajas, nos va a permitir definir unos puertos que pueden ser implementados por N adaptadores según nuestras necesidades. De esta forma podremos crear nuevos componentes en nuestra app mientras respeten dichos contratos. Estos serán intercambiables, por ejemplo, la forma en la que se buscan los libros, cómo se notifican los cambios a los usuarios o cómo se guardan los datos.
Podemos pensar en los puertos como contratos que todo adaptador debe cumplir (normalmente estos puertos suelen ser interfaces a nivel de código). A su vez podemos pensar en los adaptadores como implementaciones concretas de estos puertos en función de las necesidades de nuestra aplicación.
Como ya podéis estar pensando esto encaja perfectamente con mi idea de ir haciendo la app poco a poco, incrementando su valor. ¡Vamos a por ello!
MVP (Minimum Viable Product)
Una vez que tenemos claro qué queremos construir y cómo lo queremos hacer lo que necesitamos definir es la cantidad mínima de producto que queremos entregar lo antes posible a nuestros usuarios, aportándoles valor.
Pensando en la funcionalidad mínima con la que empezar mi app tenía claro que habría 3 componentes fundamentales:
- Un repositorio para almacenar y recuperar las subscripciones.
- Un mecanismo para notificar a los usuarios.
- Un cliente encargado de buscar libros.
Es por ello que uno de los primeros pasos fue definir estos tres puertos dentro de mi aplicación:
Puerto para persistir subscripciones
from abc import ABC, abstractmethod
from domain.subscriptions.subscription import Subscriptions
class SubscriptionsRepository(ABC):
@abstractmethod
def save(self, subscription: Subscription) -> None:
raise NotImplementedError
Puerto para buscar libros
from abc import ABC, abstractmethod
from domain.book import Book
class BookSearcher(ABC):
@abstractmethod
def find(self, query: str) -> list[Book]:
raise NotImplementedError
Puerto para notificar a los usuarios
from abc import ABC, abstractmethod
from domain.subscriptions.subscription import Subscriptions
class Notifier(ABC):
@abstractmethod
def notify(self, subscriptions: Subscriptions) -> None:
raise NotImplementedError
Una vez definidos los puertos principales el resto de la app no es nada del otro mundo. Es más, seguí la misma estructura de carpetas que en mi repositorio de Domain Driven Design, por lo que me centraré más en esta parte de Ports&Adapters.
Iteración 1: Validar la idea
Para validar que la idea tenía sentido me propuse como requisitos mínimos:
- Poder subscribirme a un autor/libro/termino.
- La persistencia de los datos sería un fichero txt.
- El input del usuario sería por consola.
- El output al usuario sería por consola.
Para cumplir con estos 4 requisitos necesité:
- Crear un adaptador llamada
TxtSubscriptionsRepository
para el puertoSubscriptionsRepository
. - Crear un adaptador llamado
ConsoleNotifier
para el puertoNotifier
. - Crear un mecanismo de delivery para interactuar con el usuario por consola.
Iteración 2: Búsqueda Múltiple
Una vez validé la idea inicial me propuse incrementar ligeramente la funcionalidad de la aplicación con las siguientes novedades:
- Poder subscribirme a uno o varios autor/libro/termino.
- Poder darse de baja de uno o a varios autor/libro/termino.
Para poder almacenar de forma sencilla múltiples términos decidí cambiar el
fichero donde se persistían los datos, un json en lugar de un txt. Para
ello solo tuve que crear un nuevo adaptador llamado JSONSubscriptionsRepository
para el puerto SubscriptionsRepository
que se encargará de la
manipulación de dicho fichero.
Como podéis ver, la diferencia entre adaptadores radica su lógica interna. Pero como todos respetan un puerto común, estos se pueden intercambiar libremente.
Iteración 3: Notificaciones por email
Una vez que mi app ya era capaz de subscribirse a múltiples términos, me pareció que era el momento de cambiar la forma en la que el usuario es notificado. En lugar de tener que ejecutar la app cada X tiempo, la propia app comprobará periódicamente si existen nuevos libros para las subscripciones y si esto ocurre enviará un email al usuario.
Este cambio fue tan sencillo como crear un adaptador llamado EmailNotifier
para el puerto
Notifier que remplazaba al ConsoleNotifier
. El desarrollo
de este nuevo adaptador lo hice completamente en paralelo aplicando TDD. Cuando lo tuve
acabado, simplemente fue cambiar un adaptador por otro.
Iteración 4: Crear una API REST
Llegados a este punto, la aplicación ya tenía el suficiente cuerpo como para seguir utilizándola por consola ya que limitaba bastante su experiencia de uso.
Al estar utilizando una arquitectura en capas he podido crear un mecanismo de delivery nuevo, en este caso una API, sin romper el funcionamiento actual de la app y llegando incluso a tener dos mecanismos de delivery en paralelo. Cuando validé que todo estaba correcto remplace la consola por la API.
Otro punto muy importante, y que no tiene tanto que ver con los puertos y los adaptadores, sino más bien con la arquitectura en capas, es que los diferentes mecanismos de delivery pueden reutilizar todas las capas internas a ellos, siendo realmente sencillo cambiar uno por otro.
Iteración 5: Persistencia en base de datos
Una vez que la app ya disponía de una API, era capaz de subscribirse a múltiples términos y notificar los cambios a través de un email al usuario, decidí que era buen momento para cambiar la persistencia a algo más tradicional. En este caso aposté por MySQL usando PlanetScale.
Para ello, y como había hecho previamente con otros puertos, simplemente creé un nuevo
adaptador llamado MySQLSubscriptionsRepository
, que implementaba el
puerto SubscriptionsRepository
.
Una vez más, este nuevo componente lo desarrollé completamente en paralelo haciendo TDD y cuando tenía la seguridad
de que funcionaba como esperaba simplemente remplace el adaptador JSONSubscriptionsRepository
por
este nuevo MySQLSubscriptionsRepository
.
Siguiente, ¿usuarios? ¿Crear una interfaz?
Llegados a este punto, las expectativas de lo que inicialmente quería crear ya están más que superadas. Ahora simplemente queda seguir pensando qué rumbo tomar y qué próximas funcionalidades añadir.
Una idea interesante puede ser crear el concepto de usuario y que las subscripciones estén vinculadas a este mismo. O tal vez crear una interfaz para consumir la API.
Como podéis ver las posibilidades son infinitas…
Conclusiones
Lo primero, gracias por llegar hasta aquí, ha sido un post denso en el que he contado muchas cosas y seguramente dejándome otras tantas por el camino.
Como hemos visto gracias al uso de ports y adapters ha sido realmente sencillo añadir nuevas funcionalidades a la aplicación.
En todos los casos estas se desarrollaron en paralelo haciendo TDD y cuando estaban listas simplemente había que remplazar un adapter por otro y listo.
Espero que os haya gustado este ejemplo real y si nunca habéis usado este tipo de arquitectura os animéis a darle una oportunidad.
!Un saludo¡