Diseña el grafo de dependencias de módulos para una app grande multi-módulo con más de 20 equipos de feature
Capas de app, core y feature, una separación API/impl que evita que las features se acoplen entre sí, y la regla de CI que mantiene honesto el grafo.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Diseña el grafo de dependencias de módulos para una app grande multi-módulo con más de 20 equipos de feature
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Aclara para qué estamos modularizando
La modularización no es gratis, así que empezaría por precisar qué problema resuelve aquí, porque con más de 20 equipos suele ser más de uno: tiempos de build (un módulo monolítico significa que cada commit de cada equipo invalida la misma caché de build incremental), fricción de merges (todos editando el mismo puñado de archivos), la necesidad de que los equipos sean dueños de un límite de código claro, y posiblemente entrega bajo demanda (enviar una feature como Dynamic Feature Module para que la instalación base se mantenga pequeña). La respuesta determina qué tan estrictos deben ser los límites: un codebase que optimiza solo por tiempo de build puede ser más laxo con las superficies de API que uno que además intenta que 20 equipos publiquen de forma independiente sin pisarse.
#El modelo de capas
El grafo tiene tres capas, y la flecha de dependencia siempre apunta en una sola dirección: hacia abajo. :app depende de los módulos de feature; los módulos de feature dependen de los módulos core; los módulos core no dependen de nada por encima de ellos. La regla que realmente importa en la práctica es la que es fácil de violar bajo presión de fecha límite: los módulos de feature nunca dependen de la implementación de otro. Sin esa regla exigida por algo más que una página de wiki, los imports cruzados "solo por esta vez" se acumulan hasta que el grafo de módulos es un monolito con archivos de Gradle de más.
#La separación API/impl, y navegación sin acoplamiento
Cada feature es en realidad dos módulos de Gradle: uno pequeño, -api, con interfaces y contratos de destino de navegación, y uno -impl con la implementación real. Otras features pueden depender de :feature-x-api; nadie fuera del equipo propietario puede depender de :feature-x-impl. Esto es lo que hace resoluble el caso "checkout necesita abrir una pantalla propiedad de search" sin que checkout importe los internals de search: checkout depende del contrato de navegación de :feature-search-api (una clave de ruta tipada), y un registro compartido en :core-navigation resuelve esa clave al destino real en tiempo de ejecución. Es inversión de dependencias aplicada a la navegación entre pantallas, y es la pieza que la mayoría de los diseños se saltan hasta que la primera solicitud real de navegación cruzada entre features fuerza el tema.
#Hacer el grafo exigible, no solo documentado
El mecanismo que mantiene esto cierto con el tiempo es una verificación de CI —un análisis del grafo de dependencias de Gradle (o un plugin construido para esto)— que falla el build en el momento en que un módulo declara un borde no permitido: un ciclo, una dependencia -impl-a--impl entre features, o un tipo concreto filtrado a través de la superficie pública de un módulo -api. CODEOWNERS mapea cada módulo al equipo que lo posee, así que un pull request que toca :feature-checkout-impl se enruta automáticamente para revisión al equipo de checkout y nadie más necesita aprobarlo. La documentación se desalinea de la realidad en menos de un trimestre; una verificación de CI en rojo no.
#Qué compra la modularización, en concreto
El modelo de build incremental y paralelo de Gradle significa que un cambio confinado a :feature-search-impl solo reconstruye y vuelve a testear ese módulo y sus dependientes, no el código de los 20 equipos. Los módulos de feature también pueden construirse y testearse contra una app de muestra pequeña en lugar del target :app completo, así que el pipeline de CI de un solo equipo para su propio pull request no necesita compilar en absoluto las features de los otros 19 equipos. Combinado con una caché de build remota compartida entre máquinas de CI y laptops de desarrolladores, esto suele ser la palanca individual más grande para mantener tiempos de build razonables a medida que crece el número de equipos: la modularización es tanto una estrategia de rendimiento de build como una de arquitectura.
#Anti-patrones que hay que nombrar
El modo de fallo más común es que :core se convierta en un vertedero: una vez que todo depende de él, un cambio de una línea en cualquier parte adentro fuerza la reconstrucción de las features de los 20 equipos, que es exactamente el cuello de botella que la modularización debía eliminar. La solución es dividir core por área de responsabilidad (core-ui, core-network, core-database, core-navigation) en lugar de un core-common gigante, para que un cambio de networking no invalide un build que solo depende del design system. Un segundo modo de fallo es filtrar tipos de -impl a través de funciones públicas de un módulo -api; el compilador no lo detecta por sí solo a menos que el módulo -api ni siquiera tenga -impl en su classpath de dependencias, que es exactamente cómo estructurar el build de Gradle para hacer la fuga imposible en lugar de solo desalentada. Un tercer modo, más sutil, es la sobrefragmentación: módulos tan pequeños que el overhead de configuración de Gradle supera cualquier ganancia de tiempo de build; agrupa clases relacionadas en vez de crear un módulo por clase.