Comunicación entre módulos en Mobile.
Las arquitecturas multi modulares en mobile se han convertido prácticamente en un must to have en el ecosistema de aplicaciones móviles. Pensar modularmente nos trae muchos beneficios, sobre todo cuando las features de una misma app se desarrollan por diferentes equipos, independizando su desarrollo y evolución.
Sin embargo cuando se tiene una arquitectura modular empezamos a sufrir de varios desafíos que en aplicaciones monolíticas podemos solucionar de una manera más sencilla y directa.
Uno de estos problemas con el que nos solemos encontrar y generalmente nos pone varias piedras en el camino, es la comunicación entre nuestros módulos.
La comunicación entre módulos es sin duda una de las piezas más importantes cuando hablamos de arquitectura modular, ya que la forma en que la tratemos nos va a diagnosticar la mayor parte de la salud de nuestros módulos.
Tener una buena salud en la conexión de nuestros módulos es imprescindible para su evolución ya que si presentamos conexiones o acoplamientos muy fuertes entre ellos va a hacer que nuestros desarrollos y despliegues se puedan ver afectados por bloqueos en las coordinaciones con los demás equipos.
Robert C. Martin, menciona en su libro clean architecture, un factor muy importante para el diseño de arquitecturas modulares; deployment independence. Nos habla que el desarrollo, evolución y despliegue de nuestros módulos deben ser independientes del ritmo de desarrollo y despliegue de otros módulos. Por ejemplo, un módulo que consuma nuestros servicios no debería verse afectado por el despliegue de una nueva versión de nuestro módulo.
El acoplamiento en la modularización al igual que cualquier diseño, es uno de los factores más importantes que debemos tener en cuenta a la hora del diseño de nuestras comunicaciones.
Cabe destacar que el buen diseño de una comunicación va a depender exclusivamente del contexto donde se encuentre la aplicación, una solución que puede funcionar para una aplicación con 100 módulos, podría no ser tan favorable en una aplicación con un par de ellos.
Sin embargo para evitar un alto acoplamiento en nuestro sistema, siempre debemos tener en cuenta las políticas de desarrollo del mismo, saber discernir cuales de ellas son de alto nivel y cuáles de ellas son de más bajo nivel. Así podremos tener nuestras políticas separadas y con todas las dependencias del código fuente apuntando en la dirección de las políticas de nivel superior.
Las políticas de bajo nivel son todas aquellas que se encuentran más cerca del input o output de nuestro sistema, son aquellas que tienden a cambiar mucho más, con mucha más urgencia, pero con menos importancia. Por el contrario, las políticas de alto nivel las encontramos definiendo nuestro modelo de negocio y tienden a cambiar con menos frecuencia pero con mayor importancia.
Vamos a ver un ejemplo de un caso de uso de una aplicación que necesita comunicarse con otro módulo para realizar una tarea y vamos a ir viendo diferentes soluciones, en las que vamos a explicar las ventajas y desventajas de cada una de ellas.
Imaginemos que estamos en nuestra aplicación de PedidosYa. Y necesitamos hacer uso de cupones de descuento, para amortizar o pagar el precio de nuestro pedido. En este caso vamos a tomar de referencia el caso de uso para aplicar un cupón de descuento a nuestro carrito de compras.
En esta transacción hay dos actores/módulos involucrados uno de ellos es el módulo de cart y el otro es el módulo de vouchers.
Imaginemos que nuestro módulo de vouchers tiene una pantalla donde se listan nuestros vouchers disponibles, cada cupón tiene un botón desde el cual vamos a poderlo aplicar a nuestro carrito de compras. Nuestro módulo de carrito de compras, va a verificar que sea un cupón válido para esta orden y procederá a hacer el descuento. Para fines de este ejemplo solo nos vamos a centrar en la interacción de los módulos a la hora de aplicar un cupón.
Para realizar esta transacción tenemos servicio expuesto en el módulo de cart que nos permite aplicar el cupón.
applyCoupon(id: String) en una clase llamada DiscountService.
Por otro lado en el módulo de vouchers tenemos un Action llamado ApplyCouponAction que se encargará de comunicarse con el servicio del módulo de cart.
1. Comunicación directa
Cómo vemos en el gráfico, nuestro action se comunica directamente con el servicio de cart, esto hace que necesitemos incluir la dependencia del servicio de cart en nuestro action (por lo menos en lenguajes estáticamente tipados) por ende nuestro action queda directamente atada al módulo de cart.
La ventaja más clara de este approach es que su implementación es relativamente sencilla, al no necesitar contemplar capas anticorrupción en el medio por parte del módulo de vouchers. Por parte del módulo de cart el uso de una interfaz para exponer este servicio, favorece el desarrollo por detrás de ella, sin afectar la interfaz.
La desventaja de incluir directamente un servicio de otro módulo en nuestra acción, es que esta conozca detalles de bajo nivel que nos van a dificultar la evolución. Así mismo estamos limitando la evolución del servicio de cart, ya que cada vez que este necesite modificarse, va a necesitar chequear no que rompa la integración con ningún otro módulo dado que si hacemos algún cambio que modifique la interfaz pública (API) lo más probable es que los módulos que dependan del modificado deban adaptarse a esa nueva interfaz.
Además nuestro proceso de build y deploy naturalmente va a ser más lento ya que debe considerar que estos contratos no se rompan.
2. Comunicación directa invirtiendo dependencias.
Esta vez entran en juego dos nuevos actores, la interfaz CouponServices y su implementación CartCouponServices, esta solución aunque puede ser un poco similar a la primera, tiene una ventaja super importante; la inversión de las dependencias para el action ApplyCouponAction, ahora el colaborador del action esta en nuestro propio dominio y es la implementación de la interfaz quién se comunica directamente con el módulo de cart.
Esto hace que por lo menos en nuestro dominio no existan dependencias a terceros y los detalles que conoce nuestro action se convierten en detalles de alto nivel y no en bajo nivel como estaban planteados en la solución anterior. Ya que nuestros detalles de alto nivel van a tender a cambiar menos y lo harán por razones más importantes. Es clave que la nueva interfaz y su implementación estén en capas separadas (por ej. Dominio e Infraestructura) para que la política de detalles quede mejor explícita.
Si bien esta solución sigue presentando los problemas de tener el módulo de cart cómo dependencia en nuestro módulo. La evolución de nuestro dominio va a ser mucho más sencilla.
Personalmente me parece esta solución adecuada para aplicaciones de pequeño a mediano tamaño donde el número de módulo crezca ocasionalmente, ya que su costo implementativo es bajo, pero puede empezar a tener complicaciones cuando el número de módulos empiece a crecer, ya que la cantidad de dependencias cruzadas puede forma una red que impida al sistema evolucionar fácilmente.
3. Comunicación invirtiendo dependencia entre módulos.
Esta solución persigue la misma filosofía que la anterior, solo que esta vez a nivel de módulos, ahora entra en juego el módulo cart-services en este módulo únicamente va a reposar los contratos de los servicios que el módulo de cart tenga con otros módulos, en este caso con nuestro módulo de vouchers. La interfaz de servicio Discount Service pasaría a el módulo cart-service y su implementación se mantendría en el módulo cart.
Como puedes ver en la imagen ahora las dependencias están invertidas, esto súper importante porque ahora el módulo cart-services se vuelve un modulo de más alto nivel, al no conocer de detalles implementativos, esto sin duda va a facilitar las evolución de los módulos de vouchers y cart separadamente y va a hacer que su despliegue sea mucho más sencillo.
Esta solución es un poco más compleja a nivel implementativo que la anterior ya que nos requiere la creación y mantenimiento de un nuevo módulo, pero se empieza a notar su gran valor en aplicaciones que tengan un número considerable de módulos, aplicaciones de mediano a mayor tamaño.
4. Comunicación a través de eventos.
Si bien esta solución se puede aplicar para este caso haciendo cierto tipo de cambios. Y si bien una solución basada en eventos es muy recomendable para la comunicación de módulos de forma desacoplada, no la vamos a mencionar en esta nota, ya que para nuestro ejercicio estamos requiriendo de la interacción con un servicio explícitamente, y no reaccionando a un evento que eventualmente pudo haber sucedido o no.
5. Comunicación a través de deeplinks.
Esta forma tiene la misma filosofía basada en eventos, en la cual la comunicación con módulos se hace a través de un tercero que es totalmente agnóstico al contexto de cualquier módulo, generalmente en el caso de los eventos sería un módulo de eventBus, en este caso nuestro módulo es Deeplink Module.
Al igual que en una navegación basada en deeplinks que se ha hecho muy popular en muchas aplicaciones, por su forma desacoplada de navegar entre diferentes flujos.
Con esta forma de comunicación vamos a tener una ruta muy similar para poder localizar el servicio de otros módulos y poder tener el resultado de vuelta en un formato que puedan interpretar las dos partes, por ej. JSON o Serializables.
En este caso vamos a tener un deeplink que puede ser similar a este para comunicarnos con el servicio de aplicar cupones en cart.
pedidosya://cart-services/apply-coupon?id=some-id
Como puedes ver en la imagen, en el módulo de cart sólo va a existir el handler que sabe responder a este deeplink y se encargará de hacer el procesamiento de la transacción y dar el resultado de vuelta a módulo de deeplinks.
Personalmente esta es mi forma favorita de comunicación en aplicaciones empresariales y de gran tamaño (donde el número de módulos crece constantemente), ya que no requerimos incluir la dependencia de cart en nuestro módulo y no necesitamos conocer detalles de bajo nivel de los servicios que usa el módulo de cart para aplicar los cupones, de esta forma la evolución de los módulos puede seguir por diferentes caminos sin bloquearse el uno al otro.
De esta forma logramos que el servicio que nos ofrece el módulo de cart, sea usable en un rango más amplio de módulos dentro de la aplicación.
Los tiempos de building y de despliegue con esta solución son mucho más ágiles ya que solo necesitamos la dependencia de deeplinkDispatcher que nos va a servir para comunicarnos con todos los módulos. Y no necesitamos chequear que no se hayan cambiado firmas de contratos antes de desplegar nuestros módulos.
Si bien esta solución tiene la desventaja de que no vamos a tener errores en tiempo de compilación por romper directamente de un contrato de una interfaz, podemos recurrir a prácticas cómo contract testing para asegurarnos que nuestros servicios siempre estén habilitados y respondiendo de la forma que esperamos.
Palabras Finales.
Para finalizar, cabe mencionar que ninguna solución es una silver bullet para todos los casos y que depende mucho del contexto donde te estés manejando, cual de ellas te rinda más ventajas.
Existen otras varias formas de comunicación que pueden tener utilidades similares, pero solo queríamos mencionar aquellas con las que hemos tenido más trayectoria en PedidosYa. Sería muy enriquecedor que nos cuentes si utilizas alguna otra técnica de comunicación que no hayamos nombrado.
Nos vemos en la próxima nota y no olvides dejar tu aplauso si la nota fue útil para tí.
Si este es un tema que te ha gustado, y trabajar con código de alta calidad es tu pasión, no dudes en revisar las posiciones que tenemos abiertas.
Revisión técnica: @demarco_ariel
Bibliografia.
Martin, Robert C. Martin. Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Robert C. Martin Series) Pearson Education.